diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 40622c5db..000000000 --- a/.gitmodules +++ /dev/null @@ -1,10 +0,0 @@ -[submodule "external/netcipher"] - path = external/netcipher - url = https://github.com/guardianproject/NetCipher.git - branch = master -[submodule "Bonsai"] - path = Bonsai - url = https://github.com/anthonycr/Bonsai.git -[submodule "AnimatedProgressBar"] - path = AnimatedProgressBar - url = https://github.com/anthonycr/AnimatedProgressBar.git diff --git a/.travis.yml b/.travis.yml index 1fc5f7032..d0075eb88 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,12 +5,8 @@ jdk: android: components: - tools - - build-tools-24.0.3 - - build-tools-23.0.3 - - build-tools-22.0.1 - - android-24 - - android-23 - - android-22 + - build-tools-25.0.3 + - android-25 - extra-android-support - extra-android-m2repository licenses: diff --git a/AnimatedProgressBar b/AnimatedProgressBar deleted file mode 160000 index 2d7f756be..000000000 --- a/AnimatedProgressBar +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2d7f756be8f562b2cedca46de7d2ff24f9383040 diff --git a/Bonsai b/Bonsai deleted file mode 160000 index 53608c03b..000000000 --- a/Bonsai +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 53608c03b77d598aaebd17eb0acbedd8ffb1bf51 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..8d1521ce8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,74 @@ +Change Log +========== + +Version 4.5.0 *(2017-06-08)* +---------------------------- +- Translation updates +- Memory leak fixes +- Basic keyboard shortcut support +- Improved browser dialogs +- Improved ad block efficiency +- Improved bookmark storage +- Added setting for black status bar +- Added downloads page +- Baidu search suggestions option +- Updated bookmark page UI +- Updated history page UI +- Fixed numerous bugs + +Version 4.4.1 *(2016-11-05)* +---------------------------- +- Fixed bookmark/tab drawer crash +- Fixed search suggestions crash on start up +- Fixed bug where links wouldn't open in the browser if you had the "restore lost tabs" option disabled + +Version 4.4.0 *(2016-10-30)* +---------------------------- +- Android 7.0 support +- Improved long-press menu +- Bookmark and tab drawers can be swapped +- Improved downloading of files +- Improved link handling for apps +- DuckDuckGo powered search suggestions +- UI improvements for readability +- Fixed text selection bugs in URL bar +- Fixed find in page bug +- Fixed image sync problems with bookmarks +- Fixed various crashes +- Fixed search suggestion bug + + +Version 4.3.3 *(2016-04-23)* +---------------------------- +- Added option to request "Do Not Track" +- Added option to hide X-Requested-With header +- Added option to hide X-Wap-Profile +- Added language support in google search suggestions +- Added home screen widget +- Added new tab button on tablet UI +- Added tab add/remove animation on both tablet and phone UI +- Added close tabs dialog (long-press back button) +- Number icon instead of hamburger icon for better usability +- Back/forward arrows change color in tablet UI to indicate that you can go back/forward +- Search bar color now matches rest of UI +- Smoothed drawer/open close experience +- Fixed intent handling vulnerability +- Fixed downloading crash (marshmallow) +- Fixed various bugs related to settings, settings not getting remembered, etc. +- Fixed bugs that happened when browser went to the background +- Fixed bugs with search bar size changing/not changing when supposed to +- Updated ad block definition +- Started to improve app architecture + +Version 4.2.3 *(2015-09-30)* +---------------------------- +- Tablet UI (optional) +- Support for Marshmallow +- Improved Reading Mode +- Added animations between screens +- Fixed some memory leaks +- Added bookmark folder support! +- Improved full-screen mode +- Settings revamp (thanks community) +- Updated icons + UI cleanup +- Added actions to the bookmarks drawer \ No newline at end of file diff --git a/LICENSE b/LICENSE index 66f4f57ab..a612ad981 100644 --- a/LICENSE +++ b/LICENSE @@ -1,10 +1,373 @@ -Copyright 2014 Anthony Restaino +Mozilla Public License Version 2.0 +================================== -Lightning Browser +1. Definitions +-------------- - This Source Code Form is subject to the terms of the - Mozilla Public License, v. 2.0. If a copy of the MPL - was not distributed with this file, You can obtain one at - - http://mozilla.org/MPL/2.0/ +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/Lightning-Browser.iml b/Lightning-Browser.iml deleted file mode 100644 index 1f7c20064..000000000 --- a/Lightning-Browser.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Mozilla Public License v. 2.0 b/Mozilla Public License v. 2.0 deleted file mode 100644 index be2cc4dfb..000000000 --- a/Mozilla Public License v. 2.0 +++ /dev/null @@ -1,362 +0,0 @@ -Mozilla Public License, version 2.0 - -1. Definitions - -1.1. "Contributor" - - means each individual or legal entity that creates, contributes to the - creation of, or owns Covered Software. - -1.2. "Contributor Version" - - means the combination of the Contributions of others (if any) used by a - Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - - means Source Code Form to which the initial Contributor has attached the - notice in Exhibit A, the Executable Form of such Source Code Form, and - Modifications of such Source Code Form, in each case including portions - thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - a. that the initial Contributor has attached the notice described in - Exhibit B to the Covered Software; or - - b. that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the terms of - a Secondary License. - -1.6. "Executable Form" - - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - - means a work that combines Covered Software with other material, in a - separate file or files, that is not Covered Software. - -1.8. "License" - - means this document. - -1.9. "Licensable" - - means having the right to grant, to the maximum extent possible, whether - at the time of the initial grant or subsequently, any and all of the - rights conveyed by this License. - -1.10. "Modifications" - - means any of the following: - - a. any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered Software; or - - b. any new file in Source Code Form that contains any Covered Software. - -1.11. "Patent Claims" of a Contributor - - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the License, - by the making, using, selling, offering for sale, having made, import, - or transfer of either its Contributions or its Contributor Version. - -1.12. "Secondary License" - - means either the GNU General Public License, Version 2.0, the GNU Lesser - General Public License, Version 2.1, the GNU Affero General Public - License, Version 3.0, or any later versions of those licenses. - -1.13. "Source Code Form" - - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that controls, is - controlled by, or is under common control with You. For purposes of this - definition, "control" means (a) the power, direct or indirect, to cause - the direction or management of such entity, whether by contract or - otherwise, or (b) ownership of more than fifty percent (50%) of the - outstanding shares or beneficial ownership of such entity. - - -2. License Grants and Conditions - -2.1. Grants - - Each Contributor hereby grants You a world-wide, royalty-free, - non-exclusive license: - - a. under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - - b. under Patent Claims of such Contributor to make, use, sell, offer for - sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - - The licenses granted in Section 2.1 with respect to any Contribution - become effective for each Contribution on the date the Contributor first - distributes such Contribution. - -2.3. Limitations on Grant Scope - - The licenses granted in this Section 2 are the only rights granted under - this License. No additional rights or licenses will be implied from the - distribution or licensing of Covered Software under this License. - Notwithstanding Section 2.1(b) above, no patent license is granted by a - Contributor: - - a. for any code that a Contributor has removed from Covered Software; or - - b. for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - - c. under Patent Claims infringed by Covered Software in the absence of - its Contributions. - - This License does not grant any rights in the trademarks, service marks, - or logos of any Contributor (except as may be necessary to comply with - the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - - No Contributor makes additional grants as a result of Your choice to - distribute the Covered Software under a subsequent version of this - License (see Section 10.2) or under the terms of a Secondary License (if - permitted under the terms of Section 3.3). - -2.5. Representation - - Each Contributor represents that the Contributor believes its - Contributions are its original creation(s) or it has sufficient rights to - grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - - This License is not intended to limit any rights You have under - applicable copyright doctrines of fair use, fair dealing, or other - equivalents. - -2.7. Conditions - - Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in - Section 2.1. - - -3. Responsibilities - -3.1. Distribution of Source Form - - All distribution of Covered Software in Source Code Form, including any - Modifications that You create or to which You contribute, must be under - the terms of this License. You must inform recipients that the Source - Code Form of the Covered Software is governed by the terms of this - License, and how they can obtain a copy of this License. You may not - attempt to alter or restrict the recipients' rights in the Source Code - Form. - -3.2. Distribution of Executable Form - - If You distribute Covered Software in Executable Form then: - - a. such Covered Software must also be made available in Source Code Form, - as described in Section 3.1, and You must inform recipients of the - Executable Form how they can obtain a copy of such Source Code Form by - reasonable means in a timely manner, at a charge no more than the cost - of distribution to the recipient; and - - b. You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter the - recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - - You may create and distribute a Larger Work under terms of Your choice, - provided that You also comply with the requirements of this License for - the Covered Software. If the Larger Work is a combination of Covered - Software with a work governed by one or more Secondary Licenses, and the - Covered Software is not Incompatible With Secondary Licenses, this - License permits You to additionally distribute such Covered Software - under the terms of such Secondary License(s), so that the recipient of - the Larger Work may, at their option, further distribute the Covered - Software under the terms of either this License or such Secondary - License(s). - -3.4. Notices - - You may not remove or alter the substance of any license notices - (including copyright notices, patent notices, disclaimers of warranty, or - limitations of liability) contained within the Source Code Form of the - Covered Software, except that You may alter any license notices to the - extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - - You may choose to offer, and to charge a fee for, warranty, support, - indemnity or liability obligations to one or more recipients of Covered - Software. However, You may do so only on Your own behalf, and not on - behalf of any Contributor. You must make it absolutely clear that any - such warranty, support, indemnity, or liability obligation is offered by - You alone, and You hereby agree to indemnify every Contributor for any - liability incurred by such Contributor as a result of warranty, support, - indemnity or liability terms You offer. You may include additional - disclaimers of warranty and limitations of liability specific to any - jurisdiction. - -4. Inability to Comply Due to Statute or Regulation - - If it is impossible for You to comply with any of the terms of this License - with respect to some or all of the Covered Software due to statute, - judicial order, or regulation then You must: (a) comply with the terms of - this License to the maximum extent possible; and (b) describe the - limitations and the code they affect. Such description must be placed in a - text file included with all distributions of the Covered Software under - this License. Except to the extent prohibited by statute or regulation, - such description must be sufficiently detailed for a recipient of ordinary - skill to be able to understand it. - -5. Termination - -5.1. The rights granted under this License will terminate automatically if You - fail to comply with any of its terms. However, if You become compliant, - then the rights granted under this License from a particular Contributor - are reinstated (a) provisionally, unless and until such Contributor - explicitly and finally terminates Your grants, and (b) on an ongoing - basis, if such Contributor fails to notify You of the non-compliance by - some reasonable means prior to 60 days after You have come back into - compliance. Moreover, Your grants from a particular Contributor are - reinstated on an ongoing basis if such Contributor notifies You of the - non-compliance by some reasonable means, this is the first time You have - received notice of non-compliance with this License from such - Contributor, and You become compliant prior to 30 days after Your receipt - of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent - infringement claim (excluding declaratory judgment actions, - counter-claims, and cross-claims) alleging that a Contributor Version - directly or indirectly infringes any patent, then the rights granted to - You by any and all Contributors for the Covered Software under Section - 2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user - license agreements (excluding distributors and resellers) which have been - validly granted by You or Your distributors under this License prior to - termination shall survive termination. - -6. Disclaimer of Warranty - - Covered Software is provided under this License on an "as is" basis, - without warranty of any kind, either expressed, implied, or statutory, - including, without limitation, warranties that the Covered Software is free - of defects, merchantable, fit for a particular purpose or non-infringing. - The entire risk as to the quality and performance of the Covered Software - is with You. Should any Covered Software prove defective in any respect, - You (not any Contributor) assume the cost of any necessary servicing, - repair, or correction. This disclaimer of warranty constitutes an essential - part of this License. No use of any Covered Software is authorized under - this License except under this disclaimer. - -7. Limitation of Liability - - Under no circumstances and under no legal theory, whether tort (including - negligence), contract, or otherwise, shall any Contributor, or anyone who - distributes Covered Software as permitted above, be liable to You for any - direct, indirect, special, incidental, or consequential damages of any - character including, without limitation, damages for lost profits, loss of - goodwill, work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses, even if such party shall have been - informed of the possibility of such damages. This limitation of liability - shall not apply to liability for death or personal injury resulting from - such party's negligence to the extent applicable law prohibits such - limitation. Some jurisdictions do not allow the exclusion or limitation of - incidental or consequential damages, so this exclusion and limitation may - not apply to You. - -8. Litigation - - Any litigation relating to this License may be brought only in the courts - of a jurisdiction where the defendant maintains its principal place of - business and such litigation shall be governed by laws of that - jurisdiction, without reference to its conflict-of-law provisions. Nothing - in this Section shall prevent a party's ability to bring cross-claims or - counter-claims. - -9. Miscellaneous - - This License represents the complete agreement concerning the subject - matter hereof. If any provision of this License is held to be - unenforceable, such provision shall be reformed only to the extent - necessary to make it enforceable. Any law or regulation which provides that - the language of a contract shall be construed against the drafter shall not - be used to construe this License against a Contributor. - - -10. Versions of the License - -10.1. New Versions - - Mozilla Foundation is the license steward. Except as provided in Section - 10.3, no one other than the license steward has the right to modify or - publish new versions of this License. Each version will be given a - distinguishing version number. - -10.2. Effect of New Versions - - You may distribute the Covered Software under the terms of the version - of the License under which You originally received the Covered Software, - or under the terms of any subsequent version published by the license - steward. - -10.3. Modified Versions - - If you create software not governed by this License, and you want to - create a new license for such software, you may create and use a - modified version of this License if you rename the license and remove - any references to the name of the license steward (except to note that - such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary - Licenses If You choose to distribute Source Code Form that is - Incompatible With Secondary Licenses under the terms of this version of - the License, the notice described in Exhibit B of this License must be - attached. - -Exhibit A - Source Code Form License Notice - - This Source Code Form is subject to the - terms of the Mozilla Public License, v. - 2.0. If a copy of the MPL was not - distributed with this file, You can - obtain one at - http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular file, -then You may include the notice in a location (such as a LICENSE file in a -relevant directory) where a recipient would be likely to look for such a -notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice - - This Source Code Form is "Incompatible - With Secondary Licenses", as defined by - the Mozilla Public License, v. 2.0. diff --git a/README.md b/README.md index 411e2cc30..43a7f3bab 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -#Lightning Browser [![Build Status](https://travis-ci.org/anthonycr/Lightning-Browser.svg?branch=master)](https://travis-ci.org/anthonycr/Lightning-Browser) -####Speed, Simplicity, Security -#![](ic_launcher_small.png) -####Download +# Lightning Browser [![Build Status](https://travis-ci.org/anthonycr/Lightning-Browser.svg?branch=master)](https://travis-ci.org/anthonycr/Lightning-Browser) + +#### Speed, Simplicity, Security +![](ic_launcher_small.png) + +#### Download * [Download APK from here](https://github.com/anthonycr/Lightning-Browser/releases) * [Download from F-Droid](https://f-droid.org/repository/browse/?fdfilter=lightning&fdid=acr.browser.lightning) @@ -10,13 +12,13 @@ * [Download Paid from Google Play](https://play.google.com/store/apps/details?id=acr.browser.lightning) -####Master Branch +#### Master Branch * [![Build Status](https://travis-ci.org/anthonycr/Lightning-Browser.svg?branch=master)](https://travis-ci.org/anthonycr/Lightning-Browser) -####Dev Branch +#### Dev Branch * [![Build Status](https://travis-ci.org/anthonycr/Lightning-Browser.svg?branch=dev)](https://travis-ci.org/anthonycr/Lightning-Browser) -####Features +#### Features * Bookmarks * History @@ -33,7 +35,7 @@ * Orbot Proxy support and I2P support -####Permissions +#### Permissions * ````INTERNET````: For accessing the web @@ -45,11 +47,11 @@ * ````ACCESS_NETWORK_STATE````: Required for the WebView to function by some OEM versions of WebKit -####The Code +#### The Code * Please contribute code back if you can. The code isn't perfect. * Please add translations/translation fixes as you see need -####Contributing +#### Contributing * [The Trello Board](https://trello.com/b/Gwjx8MC3/lightning-browser) * Contributions are always welcome * If you want a feature and can code, feel free to fork and add the change yourself and make a pull request @@ -60,15 +62,8 @@ * Prefix static member variables with 's' * Use 4 spaces instead of a tab (\t) -####Setting Up the Project -Due to the inclusion of the netcipher library for Orbot proxy support, importing the project will show you some errors. To fix this, first run the following git command in your project folder (NOTE: You need the git command installed to use this): -```` -git submodule update --init --recursive -```` -Once you run that command, the IDE should automatically import netcipher and a couple submodules in as separate projects. Than you need to set the netcipher library project as a libary of the browser project however your IDE makes you do that. Once those steps are done, the project should be all set up and ready to go. [Please read this tutorial for more information on git submodules](http://www.vogella.com/tutorials/Git/article.html#submodules) - -####License -```` +#### License +``` Copyright 2014 Anthony Restaino Lightning Browser @@ -78,7 +73,4 @@ Lightning Browser was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/ -```` -This means that you MUST provide attribution in your application to Lightning Browser for the use of this code. The way you can do this is to provide a separate screen in settings showing what open-source libraries and/or apps (this one) you used in your application. You must also open-source any files that you use from this repository and if you use any code at all from this repository, the file you put it in must be open-sourced according the the MPL 2.0 license. To put it simply, if you create a fork of this browser, your browser must be open-source, no exceptions. The only way to avoid open-sourcing a file is to completely write all the code yourself and to not use any code from Lightning. This is in order to provide a way for companies to utilize the code without making private server code public. For further explanation, please email me, or seek legal counsel :-P - -If you have any questions regarding the open-source license, please contact me at [anthonyrestaino11@gmail.com](mailto:anthonyrestaino11@gmail.com) +``` diff --git a/app/build.gradle b/app/build.gradle index 2ae1a3c3b..451c6d713 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,15 +1,14 @@ apply plugin: 'com.android.application' -apply plugin: 'com.neenbedankt.android-apt' apply plugin: 'com.getkeepsafe.dexcount' android { - compileSdkVersion 24 - buildToolsVersion "24.0.3" + compileSdkVersion 25 + buildToolsVersion '25.0.3' defaultConfig { minSdkVersion 14 - targetSdkVersion 24 - versionName "4.4.1" + targetSdkVersion 25 + versionName project.versionName vectorDrawables.useSupportLibrary = true } @@ -34,15 +33,15 @@ android { productFlavors { lightningPlus { - buildConfigField "boolean", "FULL_VERSION", "true" + buildConfigField "boolean", "FULL_VERSION", "Boolean.parseBoolean(\"true\")" applicationId "acr.browser.lightning" - versionCode 90 + versionCode project.versionCode_plus } lightningLite { - buildConfigField "boolean", "FULL_VERSION", "false" + buildConfigField "boolean", "FULL_VERSION", "Boolean.parseBoolean(\"false\")" applicationId "acr.browser.barebones" - versionCode 92 + versionCode project.versionCode_lite } } @@ -58,7 +57,7 @@ android { dexcount { includeClasses = false includeFieldCount = false - printAsTree = true + format = "tree" orderByMethodCount = true verbose = false } @@ -66,25 +65,26 @@ dexcount { dependencies { // support libraries - compile 'com.android.support:palette-v7:24.2.1' - compile 'com.android.support:appcompat-v7:24.2.1' - compile 'com.android.support:design:24.2.1' - compile 'com.android.support:recyclerview-v7:24.2.1' - compile 'com.android.support:support-v4:24.2.1' + def supportLibVersion = '25.3.1' + compile "com.android.support:palette-v7:$supportLibVersion" + compile "com.android.support:appcompat-v7:$supportLibVersion" + compile "com.android.support:design:$supportLibVersion" + compile "com.android.support:recyclerview-v7:$supportLibVersion" + compile "com.android.support:support-v4:$supportLibVersion" // html parsing for reading mode - compile 'org.jsoup:jsoup:1.9.2' - - // event bus - compile 'com.squareup:otto:1.3.8' + compile 'org.jsoup:jsoup:1.10.2' // dependency injection - compile 'com.google.dagger:dagger:2.0.2' - apt 'com.google.dagger:dagger-compiler:2.0.2' + def daggerVersion = '2.11' + compile "com.google.dagger:dagger:$daggerVersion" + annotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion" provided 'javax.annotation:jsr250-api:1.0' // view binding - compile 'com.jakewharton:butterknife:7.0.1' + def butterknifeVersion = '8.6.0' + compile "com.jakewharton:butterknife:$butterknifeVersion" + annotationProcessor "com.jakewharton:butterknife-compiler:$butterknifeVersion" // permissions compile 'com.anthonycr.grant:permissions:1.1.2' @@ -92,16 +92,19 @@ dependencies { // proxy support compile 'net.i2p.android:client:0.8' - // Use the following code to update the libnetcipher submodule - // git submodule foreach git reset --hard - // git submodule update --remote - compile project(':libnetcipher') + compile 'com.squareup.okhttp3:okhttp:3.8.0' + + // tor proxy + def netcipherVersion = '2.0.0-alpha1' + compile "info.guardianproject.netcipher:netcipher:$netcipherVersion" + compile "info.guardianproject.netcipher:netcipher-webkit:$netcipherVersion" - compile project(':bonsai') + compile 'com.anthonycr.bonsai:bonsai:1.1.0' - compile project(':animated-progress-bar') + compile 'com.anthonycr.progress:animated-progress:1.0' // memory leak analysis - debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4' - releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4' -} \ No newline at end of file + def leakCanaryVersion = '1.5.1' + debugCompile "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion" + releaseCompile "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" +} diff --git a/app/proguard-project.txt b/app/proguard-project.txt index 40cddda10..4c1bb3ddb 100644 --- a/app/proguard-project.txt +++ b/app/proguard-project.txt @@ -111,3 +111,8 @@ -dontwarn org.apache.http.conn.ssl.DefaultHostnameVerifier -dontwarn org.apache.http.HttpHost + +# Needed for okhttp +-dontwarn okio.** +-dontwarn javax.annotation.Nullable +-dontwarn javax.annotation.ParametersAreNonnullByDefault diff --git a/app/src/LightningPlus/assets/hosts.txt b/app/src/LightningPlus/assets/hosts.txt index 5377c6a1f..e9d12ca49 100644 --- a/app/src/LightningPlus/assets/hosts.txt +++ b/app/src/LightningPlus/assets/hosts.txt @@ -15,6 +15,7 @@ 127.0.0.1 007.go2cloud.org 127.0.0.1 0075-7112-e7eb-f9b9.reporo.net 127.0.0.1 008.free-counter.co.uk +127.0.0.1 00zasdf.pw 127.0.0.1 011i5.voluumtrk.com 127.0.0.1 0124498474f7c13ac9a2-6b191446002b31342189d56cabcf5227.r11.cf2.rackcdn.com 127.0.0.1 02gzx.voluumtrk.com @@ -1797,6 +1798,10 @@ 127.0.0.1 abandonglare.8m.com 127.0.0.1 abbie-surber.us 127.0.0.1 abbott.vo.llnwd.net +127.0.0.1 abbp1.pw +127.0.0.1 abbp1.science +127.0.0.1 abbp1.space +127.0.0.1 abbp1.website 127.0.0.1 abby-clowers.us 127.0.0.1 abc.apportium.com 127.0.0.1 abc.bnex.com @@ -18571,6 +18576,8 @@ 127.0.0.1 adspaes.ero-advertising.com 127.0.0.1 adspages.ero-advertising.com 127.0.0.1 adspaper.org +127.0.0.1 adspayformy.site +127.0.0.1 adspayformymortgage.win 127.0.0.1 adspcces.ero-advertising.com 127.0.0.1 adspdbl.com 127.0.0.1 adspeed.com @@ -24020,7 +24027,25 @@ 127.0.0.1 cdn1.tribalfusion.com 127.0.0.1 cdn10-ref.landing.comcontent.net 127.0.0.1 cdn11-ref.landing.comcontent.net +127.0.0.1 cdn11.00zasdf.pw +127.0.0.1 cdn11.abbp1.pw +127.0.0.1 cdn11.abbp1.science +127.0.0.1 cdn11.abbp1.space +127.0.0.1 cdn11.abbp1.website +127.0.0.1 cdn11.adspayformy.site +127.0.0.1 cdn11.adspayformymortgage.win 127.0.0.1 cdn11.contentabc.com +127.0.0.1 cdn11.monzta.pet +127.0.0.1 cdn11.oknplm.website +127.0.0.1 cdn11.olmknp.space +127.0.0.1 cdn11.pleasedontslaymy.download +127.0.0.1 cdn11.plmokn.pw +127.0.0.1 cdn11.poolnoodle.tech +127.0.0.1 cdn11.routehero.com +127.0.0.1 cdn11.tgbvfr.website +127.0.0.1 cdn11.yuiop.trade +127.0.0.1 cdn11.yuiout.online +127.0.0.1 cdn11.zxcvb.space 127.0.0.1 cdn12-ref.landing.comcontent.net 127.0.0.1 cdn13-ref.landing.comcontent.net 127.0.0.1 cdn14-ref.landing.comcontent.net @@ -26278,10 +26303,21 @@ 127.0.0.1 coxnetmasterglobal.112.2o7.net 127.0.0.1 coxnews.cimedia.com 127.0.0.1 coxpalmbeachpost.112.2o7.net +127.0.0.1 cp.abbp1.pw +127.0.0.1 cp.abbp1.science +127.0.0.1 cp.abbp1.space +127.0.0.1 cp.abbp1.website +127.0.0.1 cp.adspayformy.site 127.0.0.1 cp.doublepimp.com 127.0.0.1 cp.intl.match.com +127.0.0.1 cp.monzta.pet +127.0.0.1 cp.pleasedontslaymy.download +127.0.0.1 cp.poolnoodle.tech 127.0.0.1 cp.pushwoosh.com +127.0.0.1 cp.routehero.com 127.0.0.1 cp.surf-town.net +127.0.0.1 cp.yuiop.trade +127.0.0.1 cp.yuiout.online 127.0.0.1 cp1dk.voluumtrk.com 127.0.0.1 cpa.ly 127.0.0.1 cpaaltima.go2cloud.org @@ -29283,6 +29319,7 @@ 127.0.0.1 ftp01.dus.vmsn.de 127.0.0.1 ftpcontent.worldnow.com 127.0.0.1 ftr2.external.xerox.com +127.0.0.1 ftrackb.net 127.0.0.1 fu7fb.voluumtrk.com 127.0.0.1 fuauq.voluumtrk.com 127.0.0.1 fucktubenetwork.com @@ -29699,6 +29736,7 @@ 127.0.0.1 go-clicks.de 127.0.0.1 go.activengage.com 127.0.0.1 go.ad2up.com +127.0.0.1 go.ad2upapp.com 127.0.0.1 go.adify.com 127.0.0.1 go.adinfuse.com 127.0.0.1 go.adversal.com @@ -29707,6 +29745,7 @@ 127.0.0.1 go.by 127.0.0.1 go.coxds.com 127.0.0.1 go.cpmadvisors.com +127.0.0.1 go.deliverymodo.com 127.0.0.1 go.delnapb.com 127.0.0.1 go.feedxfeed.com 127.0.0.1 go.goroost.com @@ -33428,6 +33467,7 @@ 127.0.0.1 monitor01.con.vmsn.de 127.0.0.1 monster.gostats.com 127.0.0.1 monsterpops.com +127.0.0.1 monzta.pet 127.0.0.1 moo.go2cloud.org 127.0.0.1 mopo.ivwbox.de 127.0.0.1 mopub.web107-east.manage.com @@ -34350,6 +34390,7 @@ 127.0.0.1 oin.valuead.com 127.0.0.1 ojolink.com 127.0.0.1 ojrq.net +127.0.0.1 oknplm.website 127.0.0.1 okohp.voluumtrk.com 127.0.0.1 okpgn.voluumtrk.com 127.0.0.1 okrg0.voluumtrk.com @@ -34363,6 +34404,7 @@ 127.0.0.1 olivia-hermes.us 127.0.0.1 olivia-stanfield.us 127.0.0.1 olizyr.com +127.0.0.1 olmknp.space 127.0.0.1 om.1and1.co.uk 127.0.0.1 om.1und1.info 127.0.0.1 om.cbsi.com @@ -35207,6 +35249,7 @@ 127.0.0.1 playminigolf.com 127.0.0.1 playtomic.com 127.0.0.1 plb27.voluumtrk.com +127.0.0.1 pleasedontslaymy.download 127.0.0.1 plemedia.com 127.0.0.1 plemx.com 127.0.0.1 plenews.net @@ -35215,6 +35258,7 @@ 127.0.0.1 plex.r.worldssl.net 127.0.0.1 plex2.com 127.0.0.1 pll28.voluumtrk.com +127.0.0.1 plmokn.pw 127.0.0.1 plocia.com 127.0.0.1 ploko.voluumtrk.com 127.0.0.1 pls.webtype.com @@ -35274,10 +35318,12 @@ 127.0.0.1 pongoresume.com 127.0.0.1 pons.ivwbox.de 127.0.0.1 pontiflex.com +127.0.0.1 pool.admedo.com 127.0.0.1 pool.ads.netlog.com 127.0.0.1 pool.skyhookwireless.com 127.0.0.1 pool01.2cnt.net 127.0.0.1 pool02.2cnt.net +127.0.0.1 poolnoodle.tech 127.0.0.1 poopoo.freestats.com 127.0.0.1 pop.adcocktail.com 127.0.0.1 pop.adconjure.com @@ -36366,6 +36412,7 @@ 127.0.0.1 rotator.tradetracker.nl 127.0.0.1 rotator.trafficstars.com 127.0.0.1 rotrk.com +127.0.0.1 routehero.com 127.0.0.1 router.adlure.net 127.0.0.1 router.revmob.com 127.0.0.1 router.tlvmedia.com @@ -36476,7 +36523,6 @@ 127.0.0.1 rwbd0.voluumtrk.com 127.0.0.1 rwnkj.voluumtrk.com 127.0.0.1 rxdja.voluumtrk.com -127.0.0.1 rxf.answcdn.com 127.0.0.1 ry7c5.directadsopt.com 127.0.0.1 ryan-lietz.us 127.0.0.1 rydium.us.intellitxt.com @@ -38900,6 +38946,7 @@ 127.0.0.1 tfp.2ref.co 127.0.0.1 tga.acs86.com 127.0.0.1 tga.csbew.com +127.0.0.1 tgbvfr.website 127.0.0.1 tgp.buzzsession.com 127.0.0.1 tgp.pornsponsors.com 127.0.0.1 thaliaat02.webtrekk.net @@ -39310,6 +39357,7 @@ 127.0.0.1 track.scorpiointeractive.com 127.0.0.1 track.searchignite.com 127.0.0.1 track.sexchangegirl.com +127.0.0.1 track.sharktrkr.xyz 127.0.0.1 track.shop2market.com 127.0.0.1 track.sigfig.com 127.0.0.1 track.slideshare.net @@ -44537,7 +44585,6 @@ 127.0.0.1 webtrack-brickstreetconnect-mkt-prd.hsbc.com.hk 127.0.0.1 webtrack.jwgrant.co.uk 127.0.0.1 webtracker.educationconnection.com -127.0.0.1 webtracker.tnt.com 127.0.0.1 webtrackerplus.com 127.0.0.1 webtracking.touchclarity.com 127.0.0.1 webtraffic.ttinet.com @@ -44736,9 +44783,27 @@ 127.0.0.1 wrestling.searchwho.com 127.0.0.1 ws-eu.amazon-adsystem.com 127.0.0.1 ws-na.amazon-adsystem.com +127.0.0.1 ws.00zasdf.pw +127.0.0.1 ws.abbp1.pw +127.0.0.1 ws.abbp1.science +127.0.0.1 ws.abbp1.space +127.0.0.1 ws.abbp1.website +127.0.0.1 ws.adspayformy.site +127.0.0.1 ws.adspayformymortgage.win 127.0.0.1 ws.amazon.com +127.0.0.1 ws.monzta.pet +127.0.0.1 ws.oknplm.website +127.0.0.1 ws.olmknp.space +127.0.0.1 ws.pleasedontslaymy.download +127.0.0.1 ws.plmokn.pw +127.0.0.1 ws.poolnoodle.tech +127.0.0.1 ws.routehero.com 127.0.0.1 ws.sharethis.com 127.0.0.1 ws.tapjoyads.com +127.0.0.1 ws.tgbvfr.website +127.0.0.1 ws.yuiop.trade +127.0.0.1 ws.yuiout.online +127.0.0.1 ws.zxcvb.space 127.0.0.1 ws1.surf-town.net 127.0.0.1 ws10.surf-town.net 127.0.0.1 ws11.surf-town.net @@ -44782,6 +44847,7 @@ 127.0.0.1 wsc1.webspectator.com 127.0.0.1 wsc4.webspectator.com 127.0.0.1 wsclick.infospace.com +127.0.0.1 wsg.abbp1.pw 127.0.0.1 wsi1.surf-town.net 127.0.0.1 wsod.com 127.0.0.1 wsp1.surf-town.net @@ -44953,6 +45019,7 @@ 127.0.0.1 www.ad-lite.com 127.0.0.1 www.ad-maker.net 127.0.0.1 www.ad-media.org +127.0.0.1 www.ad-rev.in 127.0.0.1 www.ad-search.com 127.0.0.1 www.ad-solutions.com 127.0.0.1 www.ad-space.net @@ -45085,6 +45152,7 @@ 127.0.0.1 www.adsniper.ru 127.0.0.1 www.adsoogle.com 127.0.0.1 www.adspaces.ero-advertising.com +127.0.0.1 www.adspayformy.site 127.0.0.1 www.adsphinx.com 127.0.0.1 www.adspics.com 127.0.0.1 www.adspinner.com @@ -45880,6 +45948,7 @@ 127.0.0.1 www.freeviral.com 127.0.0.1 www.freewha.com 127.0.0.1 www.friendlyduck.com +127.0.0.1 www.ftrackb.net 127.0.0.1 www.fucktubenetwork.com 127.0.0.1 www.fuel451.com 127.0.0.1 www.fullqurandownload.com @@ -46682,6 +46751,7 @@ 127.0.0.1 www.pixetrk.com 127.0.0.1 www.placehold.it 127.0.0.1 www.platinscience.com +127.0.0.1 www.pleasedontslaymy.download 127.0.0.1 www.plocia.com 127.0.0.1 www.plug-media.com 127.0.0.1 www.plus500.com @@ -47727,6 +47797,8 @@ 127.0.0.1 ysm.hauchi.com.tw 127.0.0.1 ytubevideoqualitymanager.com 127.0.0.1 yuilop.com +127.0.0.1 yuiop.trade +127.0.0.1 yuiout.online 127.0.0.1 yume.com 127.0.0.1 yuppy.2cnt.net 127.0.0.1 yvap.query.yahoo.com @@ -47894,6 +47966,7 @@ 127.0.0.1 zw.zeroredirect1.com 127.0.0.1 zww.ero-advertising.com 127.0.0.1 zx.zeroredirect1.com +127.0.0.1 zxcvb.space 127.0.0.1 zxypenguin.people-group.su 127.0.0.1 zy.zeroredirect1.com 127.0.0.1 zyngawithfriends.com @@ -47901,4 +47974,4 @@ 127.0.0.1 zz.zeroredirect1.com 127.0.0.1 zzz.clickbank.net 127.0.0.1 _thums.ero-advertising.com -# Hosts: 47893 \ No newline at end of file +# Hosts: 47966 \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 80751bde2..f955ce5b3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ @@ -30,6 +30,13 @@ android:hardwareAccelerated="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/duckduckgo.png b/app/src/main/assets/duckduckgo.png index a62119e58..74091ee3c 100644 Binary files a/app/src/main/assets/duckduckgo.png and b/app/src/main/assets/duckduckgo.png differ diff --git a/app/src/main/java/acr/browser/lightning/activity/BookmarkUiModel.java b/app/src/main/java/acr/browser/lightning/activity/BookmarkUiModel.java new file mode 100644 index 000000000..457a98417 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/activity/BookmarkUiModel.java @@ -0,0 +1,48 @@ +package acr.browser.lightning.activity; + +import android.support.annotation.Nullable; + +import acr.browser.lightning.browser.BookmarksView; + +/** + * The UI model representing the current folder shown + * by the {@link BookmarksView}. + *

+ * Created by anthonycr on 5/7/17. + */ +public class BookmarkUiModel { + + @Nullable private String mCurrentFolder; + + /** + * Sets the current folder that is being shown. + * Use null as the root folder. + * + * @param folder the current folder, null for root. + */ + public void setCurrentFolder(@Nullable String folder) { + mCurrentFolder = folder; + } + + /** + * Determines if the current folder is + * the root folder. + * + * @return true if the current folder is + * the root, false otherwise. + */ + public boolean isRootFolder() { + return mCurrentFolder == null; + } + + /** + * Gets the current folder that is being shown. + * + * @return the current folder, null for root. + */ + @Nullable + public String getCurrentFolder() { + return mCurrentFolder; + } + +} diff --git a/app/src/main/java/acr/browser/lightning/activity/BrowserActivity.java b/app/src/main/java/acr/browser/lightning/activity/BrowserActivity.java index b848b0a0f..56edc7202 100644 --- a/app/src/main/java/acr/browser/lightning/activity/BrowserActivity.java +++ b/app/src/main/java/acr/browser/lightning/activity/BrowserActivity.java @@ -13,7 +13,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; -import android.database.sqlite.SQLiteException; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PorterDuff; @@ -23,7 +22,6 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Handler; import android.os.Message; import android.provider.MediaStore; import android.support.annotation.ColorInt; @@ -51,7 +49,6 @@ import android.view.View.OnClickListener; import android.view.View.OnFocusChangeListener; import android.view.View.OnKeyListener; -import android.view.View.OnLongClickListener; import android.view.View.OnTouchListener; import android.view.ViewConfiguration; import android.view.ViewGroup; @@ -80,12 +77,12 @@ import android.widget.TextView.OnEditorActionListener; import android.widget.VideoView; -import com.anthonycr.bonsai.Observable; +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableOnSubscribe; import com.anthonycr.bonsai.Schedulers; +import com.anthonycr.bonsai.SingleOnSubscribe; import com.anthonycr.grant.PermissionsManager; import com.anthonycr.progress.AnimatedProgressBar; -import com.squareup.otto.Bus; -import com.squareup.otto.Subscribe; import java.io.File; import java.io.IOException; @@ -98,15 +95,14 @@ import acr.browser.lightning.browser.BrowserPresenter; import acr.browser.lightning.browser.BrowserView; import acr.browser.lightning.browser.TabsView; -import acr.browser.lightning.bus.BookmarkEvents; -import acr.browser.lightning.bus.BrowserEvents; import acr.browser.lightning.constant.BookmarkPage; import acr.browser.lightning.constant.Constants; +import acr.browser.lightning.constant.DownloadsPage; import acr.browser.lightning.constant.HistoryPage; import acr.browser.lightning.controller.UIController; -import acr.browser.lightning.database.BookmarkManager; -import acr.browser.lightning.database.HistoryDatabase; import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.database.bookmark.BookmarkModel; +import acr.browser.lightning.database.history.HistoryModel; import acr.browser.lightning.dialog.BrowserDialog; import acr.browser.lightning.dialog.LightningDialogBuilder; import acr.browser.lightning.fragment.BookmarksFragment; @@ -115,6 +111,8 @@ import acr.browser.lightning.receiver.NetworkReceiver; import acr.browser.lightning.search.SuggestionsAdapter; import acr.browser.lightning.utils.DrawableUtils; +import acr.browser.lightning.utils.IntentUtils; +import acr.browser.lightning.utils.Preconditions; import acr.browser.lightning.utils.ProxyUtils; import acr.browser.lightning.utils.ThemeUtils; import acr.browser.lightning.utils.UrlUtils; @@ -123,12 +121,12 @@ import acr.browser.lightning.view.Handlers; import acr.browser.lightning.view.LightningView; import acr.browser.lightning.view.SearchView; -import butterknife.Bind; +import butterknife.BindView; import butterknife.ButterKnife; -public abstract class BrowserActivity extends ThemableBrowserActivity implements BrowserView, UIController, OnClickListener, OnLongClickListener { +public abstract class BrowserActivity extends ThemableBrowserActivity implements BrowserView, UIController, OnClickListener { - private static final String TAG = BrowserActivity.class.getSimpleName(); + private static final String TAG = "BrowserActivity"; private static final String INTENT_PANIC_TRIGGER = "info.guardianproject.panic.action.TRIGGER"; @@ -136,18 +134,17 @@ public abstract class BrowserActivity extends ThemableBrowserActivity implements private static final String TAG_TABS_FRAGMENT = "TAG_TABS_FRAGMENT"; // Static Layout - @Bind(R.id.drawer_layout) DrawerLayout mDrawerLayout; - @Bind(R.id.content_frame) FrameLayout mBrowserFrame; - @Bind(R.id.left_drawer) ViewGroup mDrawerLeft; - @Bind(R.id.right_drawer) ViewGroup mDrawerRight; - @Bind(R.id.ui_layout) ViewGroup mUiLayout; - @Bind(R.id.toolbar_layout) ViewGroup mToolbarLayout; - @Bind(R.id.progress_view) AnimatedProgressBar mProgressBar; - @Bind(R.id.search_bar) RelativeLayout mSearchBar; - + @BindView(R.id.drawer_layout) DrawerLayout mDrawerLayout; + @BindView(R.id.content_frame) FrameLayout mBrowserFrame; + @BindView(R.id.left_drawer) ViewGroup mDrawerLeft; + @BindView(R.id.right_drawer) ViewGroup mDrawerRight; + @BindView(R.id.ui_layout) ViewGroup mUiLayout; + @BindView(R.id.toolbar_layout) ViewGroup mToolbarLayout; + @BindView(R.id.progress_view) AnimatedProgressBar mProgressBar; + @BindView(R.id.search_bar) RelativeLayout mSearchBar; // Toolbar Views - @Bind(R.id.toolbar) Toolbar mToolbar; + @BindView(R.id.toolbar) Toolbar mToolbar; private View mSearchBackground; private SearchView mSearch; private ImageView mArrowImage; @@ -168,7 +165,7 @@ public abstract class BrowserActivity extends ThemableBrowserActivity implements private ValueCallback mUploadMessage; private ValueCallback mFilePathCallback; - // Primatives + // Primitives private boolean mFullScreen; private boolean mDarkTheme; private boolean mIsFullScreen = false; @@ -185,20 +182,13 @@ public abstract class BrowserActivity extends ThemableBrowserActivity implements private String mUntitledTitle; private String mCameraPhotoPath; - private final Handler mDrawerHandler = new Handler(); - // The singleton BookmarkManager - @Inject BookmarkManager mBookmarkManager; - - // Event bus - @Inject Bus mEventBus; + @Inject BookmarkModel mBookmarkManager; @Inject LightningDialogBuilder mBookmarksDialogBuilder; private TabsManager mTabsManager; - @Inject HistoryDatabase mHistoryDatabase; - // Image private Bitmap mWebpageBitmap; private final ColorDrawable mBackground = new ColorDrawable(); @@ -225,7 +215,8 @@ public abstract class BrowserActivity extends ThemableBrowserActivity implements public abstract void updateHistory(@Nullable final String title, @NonNull final String url); - abstract Observable updateCookiePreference(); + @NonNull + abstract Completable updateCookiePreference(); @Override protected void onCreate(Bundle savedInstanceState) { @@ -292,20 +283,27 @@ public void onDrawerStateChanged(int newState) { mWebpageBitmap = ThemeUtils.getThemedBitmap(this, R.drawable.ic_webpage, mDarkTheme); - final TabsFragment tabsFragment = new TabsFragment(); + final FragmentManager fragmentManager = getSupportFragmentManager(); + + TabsFragment tabsFragment = (TabsFragment) fragmentManager.findFragmentByTag(TAG_TABS_FRAGMENT); + BookmarksFragment bookmarksFragment = (BookmarksFragment) fragmentManager.findFragmentByTag(TAG_BOOKMARK_FRAGMENT); + + if (tabsFragment != null) { + fragmentManager.beginTransaction().remove(tabsFragment).commit(); + } + tabsFragment = TabsFragment.createTabsFragment(isIncognito(), mShowTabsInDrawer); + mTabsView = tabsFragment; - final Bundle tabsFragmentArguments = new Bundle(); - tabsFragmentArguments.putBoolean(TabsFragment.IS_INCOGNITO, isIncognito()); - tabsFragmentArguments.putBoolean(TabsFragment.VERTICAL_MODE, mShowTabsInDrawer); - tabsFragment.setArguments(tabsFragmentArguments); - final BookmarksFragment bookmarksFragment = new BookmarksFragment(); + if (bookmarksFragment != null) { + fragmentManager.beginTransaction().remove(bookmarksFragment).commit(); + } + bookmarksFragment = BookmarksFragment.createFragment(isIncognito()); + mBookmarksView = bookmarksFragment; - final Bundle bookmarksFragmentArguments = new Bundle(); - bookmarksFragmentArguments.putBoolean(BookmarksFragment.INCOGNITO_MODE, isIncognito()); - bookmarksFragment.setArguments(bookmarksFragmentArguments); - final FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.executePendingTransactions(); + fragmentManager .beginTransaction() .replace(getTabsFragmentViewId(), tabsFragment, TAG_TABS_FRAGMENT) @@ -315,8 +313,7 @@ public void onDrawerStateChanged(int newState) { mToolbarLayout.removeView(findViewById(R.id.tabs_toolbar_container)); } - if (actionBar == null) - return; + Preconditions.checkNonNull(actionBar); // set display options of the ActionBar actionBar.setDisplayShowTitleEnabled(false); @@ -337,11 +334,35 @@ public void onDrawerStateChanged(int newState) { mArrowImage.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); } updateTabNumber(0); + + // Post drawer locking in case the activity is being recreated + Handlers.MAIN.post(new Runnable() { + @Override + public void run() { + mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, getTabDrawer()); + } + }); } else { - mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, getTabDrawer()); + + // Post drawer locking in case the activity is being recreated + Handlers.MAIN.post(new Runnable() { + @Override + public void run() { + mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, getTabDrawer()); + } + }); mArrowImage.setImageResource(R.drawable.ic_action_home); mArrowImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN); } + + // Post drawer locking in case the activity is being recreated + Handlers.MAIN.post(new Runnable() { + @Override + public void run() { + mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, getBookmarkDrawer()); + } + }); + arrowButton.setOnClickListener(this); // create the search EditText in the ToolBar @@ -414,6 +435,14 @@ private int getTabsFragmentViewId() { } } + /** + * Determines if an intent is originating + * from a panic trigger. + * + * @param intent the intent to check. + * @return true if the panic trigger sent + * the intent, false otherwise. + */ static boolean isPanicTrigger(@Nullable Intent intent) { return intent != null && INTENT_PANIC_TRIGGER.equals(intent.getAction()); } @@ -423,7 +452,7 @@ void panicClean() { mTabsManager.newTab(this, "", false); mTabsManager.switchToTab(0); mTabsManager.clearSavedState(); - HistoryPage.deleteHistoryPage(getApplication()); + HistoryPage.deleteHistoryPage(getApplication()).subscribe(); closeBrowser(); // System exit needed in the case of receiving // the panic intent since finish() isn't completely @@ -520,10 +549,10 @@ public void onPreFocus() { return; } String url = currentView.getUrl(); - if (UrlUtils.isSpecialUrl(url)) { - mSearch.setText(""); - } else { - mSearch.setText(url); + if (!UrlUtils.isSpecialUrl(url)) { + if (!mSearch.hasFocus()) { + mSearch.setText(url); + } } } } @@ -532,19 +561,25 @@ private class DrawerLocker implements DrawerListener { @Override public void onDrawerClosed(View v) { - if (v == mDrawerRight && mShowTabsInDrawer) { - mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, mDrawerLeft); - } else { - mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, mDrawerRight); + View tabsDrawer = getTabDrawer(); + View bookmarksDrawer = getBookmarkDrawer(); + + if (v == tabsDrawer) { + mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, bookmarksDrawer); + } else if (mShowTabsInDrawer) { + mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, tabsDrawer); } } @Override public void onDrawerOpened(View v) { - if (v == mDrawerRight) { - mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, mDrawerLeft); + View tabsDrawer = getTabDrawer(); + View bookmarksDrawer = getBookmarkDrawer(); + + if (v == tabsDrawer) { + mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, bookmarksDrawer); } else { - mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, mDrawerRight); + mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, tabsDrawer); } } @@ -612,7 +647,6 @@ private void initializePreferences() { ((BookmarksFragment) bookmarksFragment).reinitializePreferences(); } - // TODO layout transition causing memory leak // mBrowserFrame.setLayoutTransition(new LayoutTransition()); @@ -712,6 +746,59 @@ public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { return super.onKeyUp(keyCode, event); } + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Keyboard shortcuts + if (event.isCtrlPressed() && event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_T: + // Open new tab + newTab(null, true); + return true; + case KeyEvent.KEYCODE_W: + // Close current tab + mPresenter.deleteTab(mTabsManager.indexOfCurrentTab()); + return true; + case KeyEvent.KEYCODE_Q: + // Close browser + closeBrowser(); + return true; + case KeyEvent.KEYCODE_R: + // Refresh current tab + LightningView currentTab = mTabsManager.getCurrentTab(); + if (currentTab != null) { + currentTab.reload(); + } + return true; + case KeyEvent.KEYCODE_TAB: + int nextIndex = 0; + if (event.isShiftPressed()) { + // Go back one tab + if (mTabsManager.indexOfCurrentTab() > 0) { + nextIndex = mTabsManager.indexOfCurrentTab() - 1; + } else { + nextIndex = mTabsManager.last(); + } + } else { + // Go forward one tab + if (mTabsManager.indexOfCurrentTab() < mTabsManager.last()) { + nextIndex = mTabsManager.indexOfCurrentTab() + 1; + } else { + nextIndex = 0; + } + } + mPresenter.tabChanged(nextIndex); + return true; + } + } else if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_SEARCH) { + // Highlight search field + mSearch.requestFocus(); + mSearch.selectAll(); + return true; + } + return super.dispatchKeyEvent(event); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { final LightningView currentView = mTabsManager.getCurrentTab(); @@ -748,13 +835,7 @@ public boolean onOptionsItemSelected(MenuItem item) { overridePendingTransition(R.anim.slide_up_in, R.anim.fade_out_scale); return true; case R.id.action_share: - if (currentUrl != null && !UrlUtils.isSpecialUrl(currentUrl)) { - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.setType("text/plain"); - shareIntent.putExtra(Intent.EXTRA_SUBJECT, currentView.getTitle()); - shareIntent.putExtra(Intent.EXTRA_TEXT, currentUrl); - startActivity(Intent.createChooser(shareIntent, getResources().getString(R.string.dialog_title_share))); - } + new IntentUtils(this).shareUrl(currentUrl, currentView != null ? currentView.getTitle() : null); return true; case R.id.action_bookmarks: openBookmarks(); @@ -773,6 +854,9 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.action_history: openHistory(); return true; + case R.id.action_downloads: + openDownloads(); + return true; case R.id.action_add_bookmark: if (currentUrl != null && !UrlUtils.isSpecialUrl(currentUrl)) { addBookmark(currentView.getTitle(), currentUrl); @@ -795,23 +879,37 @@ public boolean onOptionsItemSelected(MenuItem item) { // By using a manager, adds a bookmark and notifies third parties about that private void addBookmark(final String title, final String url) { - final HistoryItem item = !mBookmarkManager.isBookmark(url) - ? new HistoryItem(url, title) - : null; - if (item != null && mBookmarkManager.addBookmark(item)) { - mSuggestionsAdapter.refreshBookmarks(); - mBookmarksView.handleUpdatedUrl(url); - } + + final HistoryItem item = new HistoryItem(url, title); + mBookmarkManager.addBookmarkIfNotExists(item) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable Boolean item) { + if (Boolean.TRUE.equals(item)) { + mSuggestionsAdapter.refreshBookmarks(); + mBookmarksView.handleUpdatedUrl(url); + } + } + }); } private void deleteBookmark(final String title, final String url) { - final HistoryItem item = mBookmarkManager.isBookmark(url) - ? new HistoryItem(url, title) - : null; - if (item != null && mBookmarkManager.deleteBookmark(item)) { - mSuggestionsAdapter.refreshBookmarks(); - mBookmarksView.handleUpdatedUrl(url); - } + final HistoryItem item = new HistoryItem(url, title); + + mBookmarkManager.deleteBookmark(item) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable Boolean item) { + if (Boolean.TRUE.equals(item)) { + mSuggestionsAdapter.refreshBookmarks(); + mBookmarksView.handleUpdatedUrl(url); + } + } + }); } private void putToolbarInRoot() { @@ -956,7 +1054,7 @@ public void removeTabView() { // Use a delayed handler to make the transition smooth // otherwise it will get caught up with the showTab code // and cause a janky motion - mDrawerHandler.postDelayed(new Runnable() { + Handlers.MAIN.postDelayed(new Runnable() { @Override public void run() { mDrawerLayout.closeDrawers(); @@ -995,14 +1093,14 @@ public void setTabView(@NonNull final View view) { // Use a delayed handler to make the transition smooth // otherwise it will get caught up with the showTab code // and cause a janky motion - mDrawerHandler.postDelayed(new Runnable() { + Handlers.MAIN.postDelayed(new Runnable() { @Override public void run() { mDrawerLayout.closeDrawers(); } }, 200); - // mDrawerHandler.postDelayed(new Runnable() { + // Handlers.MAIN.postDelayed(new Runnable() { // @Override // public void run() { // Remove browser frame background to reduce overdraw @@ -1066,11 +1164,19 @@ public void bookmarkButtonClicked() { } if (!UrlUtils.isSpecialUrl(url)) { - if (!mBookmarkManager.isBookmark(url)) { - addBookmark(title, url); - } else { - deleteBookmark(title, url); - } + mBookmarkManager.isBookmark(url) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable Boolean item) { + if (Boolean.TRUE.equals(item)) { + deleteBookmark(title, url); + } else { + addBookmark(title, url); + } + } + }); } } @@ -1079,7 +1185,7 @@ public void bookmarkItemClicked(@NonNull HistoryItem item) { mPresenter.loadUrlInCurrentView(item.getUrl()); // keep any jank from happening when the drawer is closed after the // URL starts to load - mDrawerHandler.postDelayed(new Runnable() { + Handlers.MAIN.postDelayed(new Runnable() { @Override public void run() { closeDrawers(null); @@ -1087,6 +1193,11 @@ public void run() { }, 150); } + @Override + public void handleHistoryChange() { + openHistory(); + } + /** * displays the WebView contained in the LightningView Also handles the * removal of previous views @@ -1139,7 +1250,7 @@ void performExitCleanUp() { Log.d(TAG, "Cache Cleared"); } if (mPreferences.getClearHistoryExitEnabled() && !isIncognito()) { - WebUtils.clearHistory(this, mHistoryDatabase); + WebUtils.clearHistory(this); Log.d(TAG, "History Cleared"); } if (mPreferences.getClearCookiesExitEnabled() && !isIncognito()) { @@ -1249,15 +1360,13 @@ protected void onPause() { Log.d(TAG, "onPause"); mTabsManager.pauseAll(); try { - BrowserApp.get(this).unregisterReceiver(mNetworkReceiver); + getApplication().unregisterReceiver(mNetworkReceiver); } catch (IllegalArgumentException e) { Log.e(TAG, "Receiver was not registered", e); } if (isIncognito() && isFinishing()) { overridePendingTransition(R.anim.fade_in_scale, R.anim.slide_down_out); } - - mEventBus.unregister(mBusEventListener); } void saveOpenTabs() { @@ -1276,15 +1385,10 @@ protected void onStop() { protected void onDestroy() { Log.d(TAG, "onDestroy"); - mDrawerHandler.removeCallbacksAndMessages(null); + Handlers.MAIN.removeCallbacksAndMessages(null); mPresenter.shutdown(); - if (mHistoryDatabase != null) { - mHistoryDatabase.close(); - mHistoryDatabase = null; - } - super.onDestroy(); } @@ -1319,9 +1423,7 @@ protected void onResume() { IntentFilter filter = new IntentFilter(); filter.addAction(NETWORK_BROADCAST_ACTION); - BrowserApp.get(this).registerReceiver(mNetworkReceiver, filter); - - mEventBus.register(mBusEventListener); + getApplication().registerReceiver(mNetworkReceiver, filter); if (mFullScreen) { overlayToolbarOnWebView(); @@ -1481,20 +1583,15 @@ void addItemToHistory(@Nullable final String title, @NonNull final String url) { if (UrlUtils.isSpecialUrl(url)) { return; } - BrowserApp.getIOThread().execute(new Runnable() { - @Override - public void run() { - try { - mHistoryDatabase.visitHistoryItem(url, title); - } catch (IllegalStateException e) { - Log.e(TAG, "IllegalStateException in updateHistory", e); - } catch (NullPointerException e) { - Log.e(TAG, "NullPointerException in updateHistory", e); - } catch (SQLiteException e) { - Log.e(TAG, "SQLiteException in updateHistory", e); + + HistoryModel.visitHistoryItem(url, title) + .subscribeOn(Schedulers.io()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onError(@NonNull Throwable throwable) { + Log.e(TAG, "Exception while updating history", throwable); } - } - }); + }); } /** @@ -1543,7 +1640,35 @@ public void onItemClick(AdapterView adapterView, View view, int pos, long l) * function that opens the HTML history page in the browser */ private void openHistory() { - new HistoryPage(mTabsManager.getCurrentTab(), getApplication(), mHistoryDatabase).load(); + new HistoryPage().getHistoryPage() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable String item) { + Preconditions.checkNonNull(item); + LightningView view = mTabsManager.getCurrentTab(); + if (view != null) { + view.loadUrl(item); + } + } + }); + } + + private void openDownloads() { + new DownloadsPage().getDownloadsPage() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable String item) { + Preconditions.checkNonNull(item); + LightningView view = mTabsManager.getCurrentTab(); + if (view != null) { + view.loadUrl(item); + } + } + }); } private View getBookmarkDrawer() { @@ -1884,6 +2009,7 @@ public void onBackButtonPressed() { if (currentTab != null) { if (currentTab.canGoBack()) { currentTab.goBack(); + closeDrawers(null); } else { mPresenter.deleteTab(mTabsManager.positionOf(currentTab)); } @@ -1896,6 +2022,7 @@ public void onForwardButtonPressed() { if (currentTab != null) { if (currentTab.canGoForward()) { currentTab.goForward(); + closeDrawers(null); } } } @@ -2046,6 +2173,55 @@ protected void applyTransformation(float interpolatedTime, Transformation t) { } } + @Override + public void handleBookmarksChange() { + final LightningView currentTab = mTabsManager.getCurrentTab(); + if (currentTab != null && currentTab.getUrl().startsWith(Constants.FILE) + && currentTab.getUrl().endsWith(BookmarkPage.FILENAME)) { + currentTab.loadBookmarkpage(); + } + if (currentTab != null) { + mBookmarksView.handleUpdatedUrl(currentTab.getUrl()); + } + } + + @Override + public void handleDownloadDeleted() { + final LightningView currentTab = mTabsManager.getCurrentTab(); + if (currentTab != null && currentTab.getUrl().startsWith(Constants.FILE) + && currentTab.getUrl().endsWith(DownloadsPage.FILENAME)) { + currentTab.loadDownloadspage(); + } + if (currentTab != null) { + mBookmarksView.handleUpdatedUrl(currentTab.getUrl()); + } + } + + @Override + public void handleBookmarkDeleted(@NonNull HistoryItem item) { + mBookmarksView.handleBookmarkDeleted(item); + handleBookmarksChange(); + } + + @Override + public void handleNewTab(@NonNull LightningDialogBuilder.NewTab newTabType, @NonNull String url) { + mDrawerLayout.closeDrawers(); + switch (newTabType) { + case FOREGROUND: + newTab(url, true); + break; + case BACKGROUND: + newTab(url, false); + break; + case INCOGNITO: + Intent intent = new Intent(BrowserActivity.this, IncognitoActivity.class); + intent.setData(Uri.parse(url)); + startActivity(intent); + overridePendingTransition(R.anim.slide_up_in, R.anim.fade_out_scale); + break; + } + } + /** * Performs an action when the provided view is laid out. * @@ -2142,21 +2318,6 @@ public void onClick(View v) { } } - /** - * Handle long presses on views that use this class - * as their OnLongClickListener. This method should - * distinguish between the IDs of the views that are - * getting clicked. - * - * @param view the view that has been long pressed - * @return returns true since the method handles the long press - * event - */ - @Override - public boolean onLongClick(View view) { - return true; - } - /** * This NetworkReceiver notifies each of the WebViews in the browser whether * the network is currently connected or not. This is important because some @@ -2186,66 +2347,4 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis super.onRequestPermissionsResult(requestCode, permissions, grantResults); } - private final Object mBusEventListener = new Object() { - - @Subscribe - public void loadHistory(final BrowserEvents.OpenHistoryInCurrentTab event) { - new HistoryPage(mTabsManager.getCurrentTab(), getApplication(), mHistoryDatabase).load(); - } - - /** - * Load the given url in a new tab, used by the the - * {@link acr.browser.lightning.fragment.BookmarksFragment} and by the - * {@link LightningDialogBuilder} - * - * @param event Bus event indicating that the user wishes - * to open a bookmark in a new tab - */ - @Subscribe - public void loadUrlInNewTab(final BrowserEvents.OpenUrlInNewTab event) { - mDrawerLayout.closeDrawers(); - if (event.location == BrowserEvents.OpenUrlInNewTab.Location.NEW_TAB) { - newTab(event.url, true); - } else if (event.location == BrowserEvents.OpenUrlInNewTab.Location.BACKGROUND) { - newTab(event.url, false); - } else if (event.location == BrowserEvents.OpenUrlInNewTab.Location.INCOGNITO) { - Intent intent = new Intent(BrowserActivity.this, IncognitoActivity.class); - intent.setData(Uri.parse(event.url)); - startActivity(intent); - overridePendingTransition(R.anim.slide_up_in, R.anim.fade_out_scale); - } - } - - /** - * This method is called when the user edits a bookmark. - * - * @param event the event that the bookmark has changed. - */ - @Subscribe - public void bookmarkChanged(final BookmarkEvents.BookmarkChanged event) { - handleBookmarksChange(); - } - - /** - * Notify the browser that a bookmark was deleted. - * - * @param event the event that the bookmark has been deleted - */ - @Subscribe - public void bookmarkDeleted(final BookmarkEvents.Deleted event) { - handleBookmarksChange(); - } - - private void handleBookmarksChange() { - final LightningView currentTab = mTabsManager.getCurrentTab(); - if (currentTab != null && currentTab.getUrl().startsWith(Constants.FILE) - && currentTab.getUrl().endsWith(BookmarkPage.FILENAME)) { - currentTab.loadBookmarkpage(); - } - if (currentTab != null) { - mBookmarksView.handleUpdatedUrl(currentTab.getUrl()); - } - } - - }; } diff --git a/app/src/main/java/acr/browser/lightning/activity/IncognitoActivity.java b/app/src/main/java/acr/browser/lightning/activity/IncognitoActivity.java index e939e5ad8..9f085b507 100644 --- a/app/src/main/java/acr/browser/lightning/activity/IncognitoActivity.java +++ b/app/src/main/java/acr/browser/lightning/activity/IncognitoActivity.java @@ -8,19 +8,21 @@ import android.webkit.CookieManager; import android.webkit.CookieSyncManager; +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableAction; +import com.anthonycr.bonsai.CompletableSubscriber; + import acr.browser.lightning.R; -import com.anthonycr.bonsai.Action; -import com.anthonycr.bonsai.Observable; -import com.anthonycr.bonsai.Subscriber; @SuppressWarnings("deprecation") public class IncognitoActivity extends BrowserActivity { + @NonNull @Override - public Observable updateCookiePreference() { - return Observable.create(new Action() { + public Completable updateCookiePreference() { + return Completable.create(new CompletableAction() { @Override - public void onSubscribe(@NonNull Subscriber subscriber) { + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { CookieManager cookieManager = CookieManager.getInstance(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { CookieSyncManager.createInstance(IncognitoActivity.this); @@ -32,7 +34,7 @@ public void onSubscribe(@NonNull Subscriber subscriber) { } @Override - public boolean onCreateOptionsMenu(Menu menu) { + public boolean onCreateOptionsMenu(@NonNull Menu menu) { getMenuInflater().inflate(R.menu.incognito, menu); return super.onCreateOptionsMenu(menu); } diff --git a/app/src/main/java/acr/browser/lightning/activity/MainActivity.java b/app/src/main/java/acr/browser/lightning/activity/MainActivity.java index 982bec75e..058085937 100644 --- a/app/src/main/java/acr/browser/lightning/activity/MainActivity.java +++ b/app/src/main/java/acr/browser/lightning/activity/MainActivity.java @@ -8,19 +8,21 @@ import android.webkit.CookieManager; import android.webkit.CookieSyncManager; +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableAction; +import com.anthonycr.bonsai.CompletableSubscriber; + import acr.browser.lightning.R; -import com.anthonycr.bonsai.Action; -import com.anthonycr.bonsai.Observable; -import com.anthonycr.bonsai.Subscriber; @SuppressWarnings("deprecation") public class MainActivity extends BrowserActivity { + @NonNull @Override - public Observable updateCookiePreference() { - return Observable.create(new Action() { + public Completable updateCookiePreference() { + return Completable.create(new CompletableAction() { @Override - public void onSubscribe(@NonNull Subscriber subscriber) { + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { CookieManager cookieManager = CookieManager.getInstance(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { CookieSyncManager.createInstance(MainActivity.this); @@ -32,7 +34,7 @@ public void onSubscribe(@NonNull Subscriber subscriber) { } @Override - public boolean onCreateOptionsMenu(Menu menu) { + public boolean onCreateOptionsMenu(@NonNull Menu menu) { getMenuInflater().inflate(R.menu.main, menu); return super.onCreateOptionsMenu(menu); } diff --git a/app/src/main/java/acr/browser/lightning/activity/ReadingActivity.java b/app/src/main/java/acr/browser/lightning/activity/ReadingActivity.java index 90b943edd..f554c6fd8 100644 --- a/app/src/main/java/acr/browser/lightning/activity/ReadingActivity.java +++ b/app/src/main/java/acr/browser/lightning/activity/ReadingActivity.java @@ -30,28 +30,27 @@ import acr.browser.lightning.constant.Constants; import acr.browser.lightning.dialog.BrowserDialog; import acr.browser.lightning.preference.PreferenceManager; -import com.anthonycr.bonsai.Action; -import com.anthonycr.bonsai.Observable; -import com.anthonycr.bonsai.OnSubscribe; -import com.anthonycr.bonsai.Subscriber; + import com.anthonycr.bonsai.Schedulers; +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleOnSubscribe; +import com.anthonycr.bonsai.SingleSubscriber; import com.anthonycr.bonsai.Subscription; + import acr.browser.lightning.reading.HtmlFetcher; import acr.browser.lightning.reading.JResult; import acr.browser.lightning.utils.ThemeUtils; import acr.browser.lightning.utils.Utils; -import butterknife.Bind; +import butterknife.BindView; import butterknife.ButterKnife; public class ReadingActivity extends AppCompatActivity { - private static final String TAG = ReadingActivity.class.getSimpleName(); + private static final String TAG = "ReadingActivity"; - @Bind(R.id.textViewTitle) - TextView mTitle; - - @Bind(R.id.textViewBody) - TextView mBody; + @BindView(R.id.textViewTitle) TextView mTitle; + @BindView(R.id.textViewBody) TextView mBody; @Inject PreferenceManager mPreferences; @@ -132,11 +131,17 @@ public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.reading, menu); MenuItem invert = menu.findItem(R.id.invert_item); MenuItem textSize = menu.findItem(R.id.text_size_item); - int iconColor = mInvert ? ThemeUtils.getIconDarkThemeColor(this) : ThemeUtils.getIconLightThemeColor(this); - if (invert != null && invert.getIcon() != null) - invert.getIcon().setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); - if (textSize != null && textSize.getIcon() != null) - textSize.getIcon().setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); + + int iconColor = ThemeUtils.getIconThemeColor(this, mInvert); + + if (invert != null && invert.getIcon() != null) { + invert.getIcon().mutate().setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); + } + + if (textSize != null && textSize.getIcon() != null) { + textSize.getIcon().mutate().setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); + } + return super.onCreateOptionsMenu(menu); } @@ -151,56 +156,56 @@ private boolean loadPage(Intent intent) { if (getSupportActionBar() != null) getSupportActionBar().setTitle(Utils.getDomainName(mUrl)); mPageLoaderSubscription = loadPage(mUrl).subscribeOn(Schedulers.worker()) - .observeOn(Schedulers.main()) - .subscribe(new OnSubscribe() { - @Override - public void onStart() { - mProgressDialog = new ProgressDialog(ReadingActivity.this); - mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); - mProgressDialog.setCancelable(false); - mProgressDialog.setIndeterminate(true); - mProgressDialog.setMessage(getString(R.string.loading)); - mProgressDialog.show(); - BrowserDialog.setDialogSize(ReadingActivity.this, mProgressDialog); - } + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onStart() { + mProgressDialog = new ProgressDialog(ReadingActivity.this); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setCancelable(false); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setMessage(getString(R.string.loading)); + mProgressDialog.show(); + BrowserDialog.setDialogSize(ReadingActivity.this, mProgressDialog); + } - @Override - public void onNext(@Nullable ReaderInfo item) { - if (item == null || item.getTitle().isEmpty() || item.getBody().isEmpty()) { - setText(getString(R.string.untitled), getString(R.string.loading_failed)); - } else { - setText(item.getTitle(), item.getBody()); - } + @Override + public void onItem(@Nullable ReaderInfo item) { + if (item == null || item.getTitle().isEmpty() || item.getBody().isEmpty()) { + setText(getString(R.string.untitled), getString(R.string.loading_failed)); + } else { + setText(item.getTitle(), item.getBody()); } + } - @Override - public void onError(@NonNull Throwable throwable) { - setText(getString(R.string.untitled), getString(R.string.loading_failed)); - if (mProgressDialog != null && mProgressDialog.isShowing()) { - mProgressDialog.dismiss(); - mProgressDialog = null; - } + @Override + public void onError(@NonNull Throwable throwable) { + setText(getString(R.string.untitled), getString(R.string.loading_failed)); + if (mProgressDialog != null && mProgressDialog.isShowing()) { + mProgressDialog.dismiss(); + mProgressDialog = null; } + } - @Override - public void onComplete() { - if (mProgressDialog != null && mProgressDialog.isShowing()) { - mProgressDialog.dismiss(); - mProgressDialog = null; - } + @Override + public void onComplete() { + if (mProgressDialog != null && mProgressDialog.isShowing()) { + mProgressDialog.dismiss(); + mProgressDialog = null; } - }); + } + }); return true; } - private static Observable loadPage(@NonNull final String url) { - return Observable.create(new Action() { + private static Single loadPage(@NonNull final String url) { + return Single.create(new SingleAction() { @Override - public void onSubscribe(@NonNull Subscriber subscriber) { + public void onSubscribe(@NonNull SingleSubscriber subscriber) { HtmlFetcher fetcher = new HtmlFetcher(); try { JResult result = fetcher.fetchAndExtract(url, 2500, true); - subscriber.onNext(new ReaderInfo(result.getTitle(), result.getText())); + subscriber.onItem(new ReaderInfo(result.getTitle(), result.getText())); } catch (Exception e) { subscriber.onError(new Throwable("Encountered exception")); Log.e(TAG, "Error parsing page", e); @@ -290,9 +295,8 @@ public boolean onOptionsItemSelected(MenuItem item) { finish(); break; case R.id.text_size_item: - AlertDialog.Builder builder = new AlertDialog.Builder(this); - LayoutInflater inflater = this.getLayoutInflater(); - View view = inflater.inflate(R.layout.seek_layout, null); + + View view = LayoutInflater.from(this).inflate(R.layout.dialog_seek_bar, null); final SeekBar bar = (SeekBar) view.findViewById(R.id.text_size_seekbar); bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @@ -312,18 +316,20 @@ public void onStopTrackingTouch(SeekBar arg0) { }); bar.setMax(5); bar.setProgress(mTextSize); - builder.setView(view); - builder.setTitle(R.string.size); - builder.setPositiveButton(android.R.string.ok, new OnClickListener() { - @Override - public void onClick(DialogInterface arg0, int arg1) { - mTextSize = bar.getProgress(); - mBody.setTextSize(getTextSize(mTextSize)); - mPreferences.setReadingTextSize(bar.getProgress()); - } + AlertDialog.Builder builder = new AlertDialog.Builder(this) + .setView(view) + .setTitle(R.string.size) + .setPositiveButton(android.R.string.ok, new OnClickListener() { - }); + @Override + public void onClick(DialogInterface dialog, int arg1) { + mTextSize = bar.getProgress(); + mBody.setTextSize(getTextSize(mTextSize)); + mPreferences.setReadingTextSize(bar.getProgress()); + } + + }); Dialog dialog = builder.show(); BrowserDialog.setDialogSize(this, dialog); break; diff --git a/app/src/main/java/acr/browser/lightning/activity/SettingsActivity.java b/app/src/main/java/acr/browser/lightning/activity/SettingsActivity.java index 6fcdb29a9..498bd06bf 100644 --- a/app/src/main/java/acr/browser/lightning/activity/SettingsActivity.java +++ b/app/src/main/java/acr/browser/lightning/activity/SettingsActivity.java @@ -23,7 +23,7 @@ public class SettingsActivity extends ThemableSettingsActivity { - private static final List mFragments = new ArrayList<>(7); + @NonNull private static final List mFragments = new ArrayList<>(7); @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/acr/browser/lightning/activity/TabsManager.java b/app/src/main/java/acr/browser/lightning/activity/TabsManager.java index 08c85a62a..11ce58cc8 100644 --- a/app/src/main/java/acr/browser/lightning/activity/TabsManager.java +++ b/app/src/main/java/acr/browser/lightning/activity/TabsManager.java @@ -14,7 +14,15 @@ import android.util.Log; import android.webkit.WebView; -import com.squareup.otto.Bus; +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableAction; +import com.anthonycr.bonsai.CompletableSubscriber; +import com.anthonycr.bonsai.Schedulers; +import com.anthonycr.bonsai.SingleOnSubscribe; +import com.anthonycr.bonsai.Stream; +import com.anthonycr.bonsai.StreamAction; +import com.anthonycr.bonsai.StreamOnSubscribe; +import com.anthonycr.bonsai.StreamSubscriber; import java.util.ArrayList; import java.util.List; @@ -25,20 +33,13 @@ import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.constant.BookmarkPage; import acr.browser.lightning.constant.Constants; +import acr.browser.lightning.constant.DownloadsPage; import acr.browser.lightning.constant.HistoryPage; import acr.browser.lightning.constant.StartPage; -import acr.browser.lightning.database.BookmarkManager; -import acr.browser.lightning.database.HistoryDatabase; import acr.browser.lightning.dialog.BrowserDialog; import acr.browser.lightning.preference.PreferenceManager; - -import com.anthonycr.bonsai.Action; -import com.anthonycr.bonsai.Observable; -import com.anthonycr.bonsai.OnSubscribe; -import com.anthonycr.bonsai.Schedulers; -import com.anthonycr.bonsai.Subscriber; - import acr.browser.lightning.utils.FileUtils; +import acr.browser.lightning.utils.Preconditions; import acr.browser.lightning.utils.UrlUtils; import acr.browser.lightning.view.LightningView; @@ -49,22 +50,20 @@ */ public class TabsManager { - private static final String TAG = TabsManager.class.getSimpleName(); + private static final String TAG = "TabsManager"; + private static final String BUNDLE_KEY = "WEBVIEW_"; private static final String URL_KEY = "URL_KEY"; private static final String BUNDLE_STORAGE = "SAVED_TABS.parcel"; - private final List mTabList = new ArrayList<>(1); + @NonNull private final List mTabList = new ArrayList<>(1); @Nullable private LightningView mCurrentTab; @Nullable private TabNumberChangedListener mTabNumberListener; private boolean mIsInitialized = false; - private final List mPostInitializationWorkList = new ArrayList<>(); + @NonNull private final List mPostInitializationWorkList = new ArrayList<>(); @Inject PreferenceManager mPreferenceManager; - @Inject BookmarkManager mBookmarkManager; - @Inject HistoryDatabase mHistoryManager; - @Inject Bus mEventBus; @Inject Application mApp; public TabsManager() { @@ -108,13 +107,13 @@ private synchronized void finishInitialization() { * @param intent the intent that started the browser activity. * @param incognito whether or not we are in incognito mode. */ - public synchronized Observable initializeTabs(@NonNull final Activity activity, - @Nullable final Intent intent, - final boolean incognito) { - return Observable.create(new Action() { + @NonNull + public synchronized Completable initializeTabs(@NonNull final Activity activity, + @Nullable final Intent intent, + final boolean incognito) { + return Completable.create(new CompletableAction() { @Override - public void onSubscribe(@NonNull final Subscriber subscriber) { - + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { // Make sure we start with a clean tab list shutdown(); @@ -150,70 +149,111 @@ public void onSubscribe(@NonNull final Subscriber subscriber) { } private void restoreLostTabs(@Nullable final String url, @NonNull final Activity activity, - @NonNull final Subscriber subscriber) { - - restoreState().subscribeOn(Schedulers.io()) - .observeOn(Schedulers.main()).subscribe(new OnSubscribe() { - @Override - public void onNext(Bundle item) { - LightningView tab = newTab(activity, "", false); - String url = item.getString(URL_KEY); - if (url != null && tab.getWebView() != null) { - if (UrlUtils.isBookmarkUrl(url)) { - new BookmarkPage(tab, activity, mBookmarkManager).load(); - } else if (UrlUtils.isStartPageUrl(url)) { - new StartPage(tab, mApp).load(); - } else if (UrlUtils.isHistoryUrl(url)) { - new HistoryPage(tab, mApp, mHistoryManager).load(); + @NonNull final CompletableSubscriber subscriber) { + + restoreState() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new StreamOnSubscribe() { + @Override + public void onNext(@Nullable Bundle item) { + final LightningView tab = newTab(activity, "", false); + Preconditions.checkNonNull(item); + String url = item.getString(URL_KEY); + if (url != null && tab.getWebView() != null) { + if (UrlUtils.isBookmarkUrl(url)) { + new BookmarkPage(activity).getBookmarkPage() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable String item) { + Preconditions.checkNonNull(item); + tab.loadUrl(item); + } + }); + } else if (UrlUtils.isDownloadsUrl(url)) { + new DownloadsPage().getDownloadsPage() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable String item) { + Preconditions.checkNonNull(item); + tab.loadUrl(item); + } + }); + } else if (UrlUtils.isStartPageUrl(url)) { + new StartPage().getHomepage() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable String item) { + Preconditions.checkNonNull(item); + tab.loadUrl(item); + } + }); + } else if (UrlUtils.isHistoryUrl(url)) { + new HistoryPage().getHistoryPage() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable String item) { + Preconditions.checkNonNull(item); + tab.loadUrl(item); + } + }); + } + } else if (tab.getWebView() != null) { + tab.getWebView().restoreState(item); } - } else if (tab.getWebView() != null) { - tab.getWebView().restoreState(item); } - } - @Override - public void onComplete() { - if (url != null) { - if (url.startsWith(Constants.FILE)) { - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - Dialog dialog = builder.setCancelable(true) - .setTitle(R.string.title_warning) - .setMessage(R.string.message_blocked_local) - .setOnDismissListener(new DialogInterface.OnDismissListener() { - @Override - public void onDismiss(DialogInterface dialog) { - if (mTabList.isEmpty()) { - newTab(activity, null, false); + @Override + public void onComplete() { + if (url != null) { + if (url.startsWith(Constants.FILE)) { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + Dialog dialog = builder.setCancelable(true) + .setTitle(R.string.title_warning) + .setMessage(R.string.message_blocked_local) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + if (mTabList.isEmpty()) { + newTab(activity, null, false); + } + finishInitialization(); + subscriber.onComplete(); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.action_open, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + newTab(activity, url, false); } - finishInitialization(); - subscriber.onComplete(); - } - }) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.action_open, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - newTab(activity, url, false); - } - }).show(); - BrowserDialog.setDialogSize(activity, dialog); + }).show(); + BrowserDialog.setDialogSize(activity, dialog); + } else { + newTab(activity, url, false); + if (mTabList.isEmpty()) { + newTab(activity, null, false); + } + finishInitialization(); + subscriber.onComplete(); + } } else { - newTab(activity, url, false); if (mTabList.isEmpty()) { newTab(activity, null, false); } finishInitialization(); subscriber.onComplete(); } - } else { - if (mTabList.isEmpty()) { - newTab(activity, null, false); - } - finishInitialization(); - subscriber.onComplete(); } - } - }); + }); } /** @@ -443,7 +483,7 @@ public synchronized int positionOf(final LightningView tab) { */ public void saveState() { Bundle outState = new Bundle(ClassLoader.getSystemClassLoader()); - Log.d(Constants.TAG, "Saving tab state"); + Log.d(TAG, "Saving tab state"); for (int n = 0; n < mTabList.size(); n++) { LightningView tab = mTabList.get(n); if (TextUtils.isEmpty(tab.getUrl())) { @@ -477,13 +517,13 @@ public void clearSavedState() { * and will delete the saved instance file when * restoration is complete. */ - private Observable restoreState() { - return Observable.create(new Action() { + private Stream restoreState() { + return Stream.create(new StreamAction() { @Override - public void onSubscribe(@NonNull Subscriber subscriber) { + public void onSubscribe(@NonNull StreamSubscriber subscriber) { Bundle savedState = FileUtils.readBundleFromStorage(mApp, BUNDLE_STORAGE); if (savedState != null) { - Log.d(Constants.TAG, "Restoring previous WebView state now"); + Log.d(TAG, "Restoring previous WebView state now"); for (String key : savedState.keySet()) { if (key.startsWith(BUNDLE_KEY)) { subscriber.onNext(savedState.getBundle(key)); @@ -496,17 +536,6 @@ public void onSubscribe(@NonNull Subscriber subscriber) { }); } - /** - * Return the {@link WebView} associated to the current tab, - * or null if there is no current tab. - * - * @return a {@link WebView} or null if there is no current tab. - */ - @Nullable - public synchronized WebView getCurrentWebView() { - return mCurrentTab != null ? mCurrentTab.getWebView() : null; - } - /** * Returns the index of the current tab. * @@ -538,6 +567,26 @@ public synchronized LightningView getCurrentTab() { return mCurrentTab; } + /** + * Returns the {@link LightningView} with + * the provided hash, or null if there is + * no tab with the hash. + * + * @param hashCode the hashcode. + * @return the tab with an identical hash, or null. + */ + @Nullable + public synchronized LightningView getTabForHashCode(int hashCode) { + for (LightningView tab : mTabList) { + if (tab.getWebView() != null) { + if (tab.getWebView().hashCode() == hashCode) { + return tab; + } + } + } + return null; + } + /** * Switch the current tab to the one at the given position. * It returns the selected tab that has been switced to. diff --git a/app/src/main/java/acr/browser/lightning/activity/ThemableBrowserActivity.java b/app/src/main/java/acr/browser/lightning/activity/ThemableBrowserActivity.java index b4d112fdd..3a29986da 100644 --- a/app/src/main/java/acr/browser/lightning/activity/ThemableBrowserActivity.java +++ b/app/src/main/java/acr/browser/lightning/activity/ThemableBrowserActivity.java @@ -2,6 +2,8 @@ import android.content.Intent; import android.content.res.Configuration; +import android.graphics.Color; +import android.os.Build; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; @@ -10,6 +12,7 @@ import acr.browser.lightning.R; import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.preference.PreferenceManager; +import acr.browser.lightning.utils.ThemeUtils; public abstract class ThemableBrowserActivity extends AppCompatActivity { @@ -32,6 +35,18 @@ protected void onCreate(Bundle savedInstanceState) { setTheme(R.style.Theme_BlackTheme); } super.onCreate(savedInstanceState); + + resetPreferences(); + } + + private void resetPreferences() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (mPreferences.getUseBlackStatusBar()) { + getWindow().setStatusBarColor(Color.BLACK); + } else { + getWindow().setStatusBarColor(ThemeUtils.getStatusBarColor(this)); + } + } } @Override @@ -56,6 +71,7 @@ void onWindowVisibleToUserAfterResume() { @Override protected void onResume() { super.onResume(); + resetPreferences(); mShouldRunOnResumeActions = true; int theme = mPreferences.getUseTheme(); boolean drawerTabs = mPreferences.getShowTabsInDrawer(!isTablet()); diff --git a/app/src/main/java/acr/browser/lightning/activity/ThemableSettingsActivity.java b/app/src/main/java/acr/browser/lightning/activity/ThemableSettingsActivity.java index 4d3cbe5f9..ebc2c1e78 100644 --- a/app/src/main/java/acr/browser/lightning/activity/ThemableSettingsActivity.java +++ b/app/src/main/java/acr/browser/lightning/activity/ThemableSettingsActivity.java @@ -1,6 +1,8 @@ package acr.browser.lightning.activity; +import android.graphics.Color; import android.graphics.drawable.ColorDrawable; +import android.os.Build; import android.os.Bundle; import javax.inject.Inject; @@ -14,12 +16,12 @@ public abstract class ThemableSettingsActivity extends AppCompatPreferenceActivi private int mTheme; - @Inject PreferenceManager mPreferenceManager; + @Inject PreferenceManager mPreferences; @Override protected void onCreate(Bundle savedInstanceState) { BrowserApp.getAppComponent().inject(this); - mTheme = mPreferenceManager.getUseTheme(); + mTheme = mPreferences.getUseTheme(); // set the theme if (mTheme == 0) { @@ -33,12 +35,25 @@ protected void onCreate(Bundle savedInstanceState) { this.getWindow().setBackgroundDrawable(new ColorDrawable(ThemeUtils.getPrimaryColorDark(this))); } super.onCreate(savedInstanceState); + + resetPreferences(); + } + + private void resetPreferences() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (mPreferences.getUseBlackStatusBar()) { + getWindow().setStatusBarColor(Color.BLACK); + } else { + getWindow().setStatusBarColor(ThemeUtils.getStatusBarColor(this)); + } + } } @Override protected void onResume() { super.onResume(); - if (mPreferenceManager.getUseTheme() != mTheme) { + resetPreferences(); + if (mPreferences.getUseTheme() != mTheme) { restart(); } } diff --git a/app/src/main/java/acr/browser/lightning/animation/AnimationUtils.java b/app/src/main/java/acr/browser/lightning/animation/AnimationUtils.java new file mode 100644 index 000000000..825144847 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/animation/AnimationUtils.java @@ -0,0 +1,52 @@ +package acr.browser.lightning.animation; + +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Animation; +import android.view.animation.Transformation; +import android.widget.ImageView; + +/** + * Animation specific helper code. + */ +public class AnimationUtils { + + /** + * Creates an animation that rotates an {@link ImageView} + * around the Y axis by 180 degrees and changes the image + * resource shown when the view is rotated 90 degrees to the user. + * + * @param imageView the view to rotate. + * @param drawableRes the drawable to set when the view + * is rotated by 90 degrees. + * @return an animation that will change the image shown by the view. + */ + @NonNull + public static Animation createRotationTransitionAnimation(@NonNull final ImageView imageView, + @DrawableRes final int drawableRes) { + Animation animation = new Animation() { + + private boolean mSetFinalDrawable; + + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + if (interpolatedTime < 0.5f) { + imageView.setRotationY(90 * interpolatedTime * 2f); + } else { + if (!mSetFinalDrawable) { + mSetFinalDrawable = true; + imageView.setImageResource(drawableRes); + } + imageView.setRotationY((-90) + (90 * (interpolatedTime - 0.5f) * 2f)); + } + } + }; + + animation.setDuration(300); + animation.setInterpolator(new AccelerateDecelerateInterpolator()); + + return animation; + } + +} diff --git a/app/src/main/java/acr/browser/lightning/app/AppComponent.java b/app/src/main/java/acr/browser/lightning/app/AppComponent.java index c28da576e..8ca9d5c4f 100644 --- a/app/src/main/java/acr/browser/lightning/app/AppComponent.java +++ b/app/src/main/java/acr/browser/lightning/app/AppComponent.java @@ -8,7 +8,11 @@ import acr.browser.lightning.activity.ThemableBrowserActivity; import acr.browser.lightning.activity.ThemableSettingsActivity; import acr.browser.lightning.browser.BrowserPresenter; +import acr.browser.lightning.constant.BookmarkPage; +import acr.browser.lightning.constant.DownloadsPage; +import acr.browser.lightning.constant.HistoryPage; import acr.browser.lightning.constant.StartPage; +import acr.browser.lightning.database.history.HistoryDatabase; import acr.browser.lightning.dialog.LightningDialogBuilder; import acr.browser.lightning.download.LightningDownloadListener; import acr.browser.lightning.fragment.BookmarkSettingsFragment; @@ -18,8 +22,8 @@ import acr.browser.lightning.fragment.PrivacySettingsFragment; import acr.browser.lightning.fragment.TabsFragment; import acr.browser.lightning.search.SuggestionsAdapter; -import acr.browser.lightning.utils.AdBlock; import acr.browser.lightning.utils.ProxyUtils; +import acr.browser.lightning.view.LightningChromeClient; import acr.browser.lightning.view.LightningView; import acr.browser.lightning.view.LightningWebClient; import dagger.Component; @@ -54,14 +58,18 @@ public interface AppComponent { void inject(ThemableSettingsActivity activity); - void inject(AdBlock adBlock); - void inject(LightningDownloadListener listener); void inject(PrivacySettingsFragment fragment); void inject(StartPage startPage); + void inject(HistoryPage historyPage); + + void inject(BookmarkPage bookmarkPage); + + void inject(DownloadsPage downloadsPage); + void inject(BrowserPresenter presenter); void inject(TabsManager manager); @@ -70,4 +78,8 @@ public interface AppComponent { void inject(SuggestionsAdapter suggestionsAdapter); + void inject(LightningChromeClient chromeClient); + + HistoryDatabase historyDatabase(); + } diff --git a/app/src/main/java/acr/browser/lightning/app/AppModule.java b/app/src/main/java/acr/browser/lightning/app/AppModule.java index 9a4811eef..400d78374 100644 --- a/app/src/main/java/acr/browser/lightning/app/AppModule.java +++ b/app/src/main/java/acr/browser/lightning/app/AppModule.java @@ -4,23 +4,23 @@ import android.content.Context; import android.support.annotation.NonNull; -import com.squareup.otto.Bus; - import net.i2p.android.ui.I2PAndroidHelper; import javax.inject.Singleton; +import acr.browser.lightning.database.bookmark.BookmarkDatabase; +import acr.browser.lightning.database.bookmark.BookmarkModel; +import acr.browser.lightning.database.downloads.DownloadsDatabase; +import acr.browser.lightning.database.downloads.DownloadsModel; import dagger.Module; import dagger.Provides; @Module public class AppModule { private final BrowserApp mApp; - @NonNull private final Bus mBus; public AppModule(BrowserApp app) { this.mApp = app; - this.mBus = new Bus(); } @Provides @@ -35,8 +35,16 @@ public Context provideContext() { @NonNull @Provides - public Bus provideBus() { - return mBus; + @Singleton + public BookmarkModel provideBookmarkMode() { + return new BookmarkDatabase(mApp); + } + + @NonNull + @Provides + @Singleton + public DownloadsModel provideDownloadsMode() { + return new DownloadsDatabase(mApp); } @NonNull diff --git a/app/src/main/java/acr/browser/lightning/app/BrowserApp.java b/app/src/main/java/acr/browser/lightning/app/BrowserApp.java index 6bf6d734c..1bbc19a56 100644 --- a/app/src/main/java/acr/browser/lightning/app/BrowserApp.java +++ b/app/src/main/java/acr/browser/lightning/app/BrowserApp.java @@ -8,32 +8,45 @@ import android.os.Build; import android.os.StrictMode; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatDelegate; import android.util.Log; import android.webkit.WebView; +import com.anthonycr.bonsai.Schedulers; import com.squareup.leakcanary.LeakCanary; -import com.squareup.otto.Bus; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; +import java.util.List; import javax.inject.Inject; import acr.browser.lightning.BuildConfig; +import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.database.bookmark.BookmarkExporter; +import acr.browser.lightning.database.bookmark.legacy.LegacyBookmarkManager; +import acr.browser.lightning.database.bookmark.BookmarkModel; import acr.browser.lightning.preference.PreferenceManager; import acr.browser.lightning.utils.FileUtils; import acr.browser.lightning.utils.MemoryLeakUtils; +import acr.browser.lightning.utils.Preconditions; public class BrowserApp extends Application { - private static final String TAG = BrowserApp.class.getSimpleName(); + private static final String TAG = "BrowserApp"; - private static AppComponent mAppComponent; - private static final Executor mIOThread = Executors.newSingleThreadExecutor(); - private static final Executor mTaskThread = Executors.newCachedThreadPool(); + static { + AppCompatDelegate.setCompatVectorFromResourcesEnabled(Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT); + } + + @Nullable private static AppComponent sAppComponent; - @Inject Bus mBus; @Inject PreferenceManager mPreferenceManager; + @Inject BookmarkModel mBookmarkModel; + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + } @Override public void onCreate() { @@ -53,7 +66,7 @@ public void onCreate() { Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override - public void uncaughtException(Thread thread, Throwable ex) { + public void uncaughtException(Thread thread, @NonNull Throwable ex) { if (BuildConfig.DEBUG) { FileUtils.writeCrashToStorage(ex); @@ -67,8 +80,24 @@ public void uncaughtException(Thread thread, Throwable ex) { } }); - mAppComponent = DaggerAppComponent.builder().appModule(new AppModule(this)).build(); - mAppComponent.inject(this); + sAppComponent = DaggerAppComponent.builder().appModule(new AppModule(this)).build(); + sAppComponent.inject(this); + + Schedulers.worker().execute(new Runnable() { + @Override + public void run() { + List oldBookmarks = LegacyBookmarkManager.destructiveGetBookmarks(BrowserApp.this); + + if (!oldBookmarks.isEmpty()) { + // If there are old bookmarks, import them + mBookmarkModel.addBookmarkList(oldBookmarks).subscribeOn(Schedulers.io()).subscribe(); + } else if (mBookmarkModel.count() == 0) { + // If the database is empty, fill it from the assets list + List assetsBookmarks = BookmarkExporter.importBookmarksFromAssets(BrowserApp.this); + mBookmarkModel.addBookmarkList(assetsBookmarks).subscribeOn(Schedulers.io()).subscribe(); + } + } + }); if (mPreferenceManager.getUseLeakCanary() && !isRelease()) { LeakCanary.install(this); @@ -87,26 +116,9 @@ public void onActivityDestroyed(Activity activity) { } @NonNull - public static BrowserApp get(@NonNull Context context) { - return (BrowserApp) context.getApplicationContext(); - } - public static AppComponent getAppComponent() { - return mAppComponent; - } - - @NonNull - public static Executor getIOThread() { - return mIOThread; - } - - @NonNull - public static Executor getTaskThread() { - return mTaskThread; - } - - public static Bus getBus(@NonNull Context context) { - return get(context).mBus; + Preconditions.checkNonNull(sAppComponent); + return sAppComponent; } /** diff --git a/app/src/main/java/acr/browser/lightning/async/AsyncExecutor.java b/app/src/main/java/acr/browser/lightning/async/AsyncExecutor.java deleted file mode 100644 index 7dea22476..000000000 --- a/app/src/main/java/acr/browser/lightning/async/AsyncExecutor.java +++ /dev/null @@ -1,53 +0,0 @@ -package acr.browser.lightning.async; - -import android.support.annotation.NonNull; -import android.util.Log; - -import java.util.ArrayDeque; -import java.util.Queue; -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.RejectedExecutionException; - -/** - * Created 9/27/2015 Anthony Restaino - */ -public class AsyncExecutor implements Executor { - - private static final String TAG = AsyncExecutor.class.getSimpleName(); - private static final AsyncExecutor INSTANCE = new AsyncExecutor(); - private final Queue mQueue = new ArrayDeque<>(1); - private final ExecutorService mExecutor = Executors.newFixedThreadPool(4); - - private AsyncExecutor() {} - - @NonNull - public static AsyncExecutor getInstance() { - return INSTANCE; - } - - public synchronized void notifyThreadFinish() { - if (mQueue.isEmpty()) { - return; - } - Runnable runnable = mQueue.remove(); - execute(runnable); - } - - @Override - protected void finalize() throws Throwable { - mExecutor.shutdownNow(); - super.finalize(); - } - - @Override - public void execute(@NonNull Runnable command) { - try { - mExecutor.execute(command); - } catch (RejectedExecutionException ignored) { - mQueue.add(command); - Log.d(TAG, "Thread was enqueued"); - } - } -} diff --git a/app/src/main/java/acr/browser/lightning/async/ImageDownloadTask.java b/app/src/main/java/acr/browser/lightning/async/ImageDownloadTask.java deleted file mode 100644 index 60cd9bfd4..000000000 --- a/app/src/main/java/acr/browser/lightning/async/ImageDownloadTask.java +++ /dev/null @@ -1,156 +0,0 @@ -package acr.browser.lightning.async; - -import android.app.Application; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.os.AsyncTask; -import android.support.annotation.NonNull; -import android.util.Log; -import android.widget.ImageView; - -import com.anthonycr.bonsai.Schedulers; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.lang.ref.WeakReference; -import java.net.HttpURLConnection; -import java.net.URL; - -import acr.browser.lightning.constant.Constants; -import acr.browser.lightning.database.HistoryItem; -import acr.browser.lightning.utils.Utils; - -public class ImageDownloadTask extends AsyncTask { - - private static final String TAG = ImageDownloadTask.class.getSimpleName(); - @NonNull private final WeakReference mFaviconImage; - @NonNull private final Application mContext; - @NonNull private final HistoryItem mWeb; - private final String mUrl; - @NonNull private final Bitmap mDefaultBitmap; - - public ImageDownloadTask(@NonNull ImageView bmImage, - @NonNull HistoryItem web, - @NonNull Bitmap defaultBitmap, - @NonNull Application context) { - // Set a tag on the ImageView so we know if the view - // has gone out of scope and should not be used - bmImage.setTag(web.getUrl().hashCode()); - this.mFaviconImage = new WeakReference<>(bmImage); - this.mWeb = web; - this.mUrl = web.getUrl(); - this.mDefaultBitmap = defaultBitmap; - this.mContext = context; - } - - @NonNull - @Override - protected Bitmap doInBackground(Void... params) { - Bitmap mIcon = null; - // unique path for each url that is bookmarked. - if (mUrl == null) { - return mDefaultBitmap; - } - File cache = mContext.getCacheDir(); - final Uri uri = Uri.parse(mUrl); - if (uri.getHost() == null || uri.getScheme() == null) { - return mDefaultBitmap; - } - final String hash = String.valueOf(uri.getHost().hashCode()); - final File image = new File(cache, hash + ".png"); - final String urlDisplay = uri.getScheme() + "://" + uri.getHost() + "/favicon.ico"; - if (Constants.FILE.startsWith(uri.getScheme())) { - return mDefaultBitmap; - } - // checks to see if the image exists - if (!image.exists()) { - FileOutputStream fos = null; - InputStream in = null; - try { - // if not, download it... - final URL urlDownload = new URL(urlDisplay); - final HttpURLConnection connection = (HttpURLConnection) urlDownload.openConnection(); - connection.setDoInput(true); - connection.setConnectTimeout(1000); - connection.setReadTimeout(1000); - connection.connect(); - in = connection.getInputStream(); - - if (in != null) { - mIcon = BitmapFactory.decodeStream(in); - } - // ...and cache it - if (mIcon != null) { - fos = new FileOutputStream(image); - mIcon.compress(Bitmap.CompressFormat.PNG, 100, fos); - fos.flush(); - Log.d(Constants.TAG, "Downloaded: " + urlDisplay); - } - - } catch (Exception ignored) { - Log.d(TAG, "Could not download: " + urlDisplay); - } finally { - Utils.close(in); - Utils.close(fos); - } - } else { - // if it exists, retrieve it from the cache - mIcon = BitmapFactory.decodeFile(image.getPath()); - } - if (mIcon == null) { - InputStream in = null; - FileOutputStream fos = null; - try { - // if not, download it... - final URL urlDownload = new URL("https://www.google.com/s2/favicons?domain_url=" + uri.toString()); - final HttpURLConnection connection = (HttpURLConnection) urlDownload.openConnection(); - connection.setDoInput(true); - connection.setConnectTimeout(1000); - connection.setReadTimeout(1000); - connection.connect(); - in = connection.getInputStream(); - - if (in != null) { - mIcon = BitmapFactory.decodeStream(in); - } - // ...and cache it - if (mIcon != null) { - fos = new FileOutputStream(image); - mIcon.compress(Bitmap.CompressFormat.PNG, 100, fos); - fos.flush(); - } - - } catch (Exception e) { - Log.d(TAG, "Could not download Google favicon"); - } finally { - Utils.close(in); - Utils.close(fos); - } - } - if (mIcon == null) { - return mDefaultBitmap; - } else { - return mIcon; - } - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - super.onPostExecute(bitmap); - AsyncExecutor.getInstance().notifyThreadFinish(); - final Bitmap fav = Utils.padFavicon(bitmap); - final ImageView view = mFaviconImage.get(); - if (view != null && view.getTag().equals(mWeb.getUrl().hashCode())) { - Schedulers.main().execute(new Runnable() { - @Override - public void run() { - view.setImageBitmap(fav); - } - }); - } - mWeb.setBitmap(fav); - } - -} diff --git a/app/src/main/java/acr/browser/lightning/browser/BookmarksView.java b/app/src/main/java/acr/browser/lightning/browser/BookmarksView.java index de578ca57..c09b4c789 100644 --- a/app/src/main/java/acr/browser/lightning/browser/BookmarksView.java +++ b/app/src/main/java/acr/browser/lightning/browser/BookmarksView.java @@ -2,10 +2,14 @@ import android.support.annotation.NonNull; +import acr.browser.lightning.database.HistoryItem; + public interface BookmarksView { void navigateBack(); void handleUpdatedUrl(@NonNull String url); + void handleBookmarkDeleted(@NonNull HistoryItem item); + } diff --git a/app/src/main/java/acr/browser/lightning/browser/BrowserPresenter.java b/app/src/main/java/acr/browser/lightning/browser/BrowserPresenter.java index 2c19fbf31..5ccfa72d2 100644 --- a/app/src/main/java/acr/browser/lightning/browser/BrowserPresenter.java +++ b/app/src/main/java/acr/browser/lightning/browser/BrowserPresenter.java @@ -7,11 +7,12 @@ import android.support.annotation.Nullable; import android.util.Log; +import com.anthonycr.bonsai.CompletableOnSubscribe; import com.anthonycr.bonsai.Schedulers; -import com.squareup.otto.Bus; import javax.inject.Inject; +import acr.browser.lightning.BuildConfig; import acr.browser.lightning.R; import acr.browser.lightning.activity.TabsManager; import acr.browser.lightning.app.BrowserApp; @@ -19,8 +20,6 @@ import acr.browser.lightning.controller.UIController; import acr.browser.lightning.preference.PreferenceManager; -import com.anthonycr.bonsai.OnSubscribe; - import acr.browser.lightning.utils.UrlUtils; import acr.browser.lightning.view.LightningView; @@ -31,11 +30,10 @@ */ public class BrowserPresenter { - private static final String TAG = BrowserPresenter.class.getSimpleName(); + private static final String TAG = "BrowserPresenter"; @NonNull private final TabsManager mTabsModel; @Inject PreferenceManager mPreferences; - @Inject Bus mEventBus; @NonNull private final BrowserView mView; @Nullable private LightningView mCurrentTab; @@ -65,7 +63,7 @@ public void tabNumberChanged(int newNumber) { public void setupTabs(@Nullable Intent intent) { mTabsModel.initializeTabs((Activity) mView, intent, mIsIncognito) .subscribeOn(Schedulers.main()) - .subscribe(new OnSubscribe() { + .subscribe(new CompletableOnSubscribe() { @Override public void onComplete() { // At this point we always have at least a tab in the tab manager @@ -108,12 +106,12 @@ private void onTabChanged(@Nullable LightningView newTab) { // TODO: Restore this when Google fixes the bug where the WebView is // blank after calling onPause followed by onResume. // mCurrentTab.onPause(); - mCurrentTab.setForegroundTab(false); + mCurrentTab.setIsForegroundTab(false); } newTab.resumeTimers(); newTab.onResume(); - newTab.setForegroundTab(true); + newTab.setIsForegroundTab(true); mView.updateProgress(newTab.getProgress()); mView.setBackButtonEnabled(newTab.canGoBack()); @@ -164,10 +162,10 @@ public void deleteTab(int position) { } final boolean isShown = tabToDelete.isShown(); - boolean shouldClose = mShouldClose && isShown && Boolean.TRUE.equals(tabToDelete.getTag()); + boolean shouldClose = mShouldClose && isShown && tabToDelete.isNewTab(); final LightningView currentTab = mTabsModel.getCurrentTab(); if (mTabsModel.size() == 1 && currentTab != null && - (UrlUtils.isSpecialUrl(currentTab.getUrl()) || + (UrlUtils.isStartPageUrl(currentTab.getUrl()) || currentTab.getUrl().equals(mPreferences.getHomepage()))) { mView.closeActivity(); return; @@ -223,13 +221,16 @@ public void run() { } else { url = null; } - int num = 0; + int tabHashCode = 0; if (intent != null && intent.getExtras() != null) { - num = intent.getExtras().getInt(Constants.INTENT_ORIGIN); + tabHashCode = intent.getExtras().getInt(Constants.INTENT_ORIGIN); } - if (num == 1) { - loadUrlInCurrentView(url); + if (tabHashCode != 0) { + LightningView tab = mTabsModel.getTabForHashCode(tabHashCode); + if (tab != null) { + tab.loadUrl(url); + } } else if (url != null) { if (url.startsWith(Constants.FILE)) { mView.showBlockedLocalFileDialog(new DialogInterface.OnClickListener() { @@ -239,7 +240,7 @@ public void onClick(DialogInterface dialog, int which) { mShouldClose = true; LightningView tab = mTabsModel.lastTab(); if (tab != null) { - tab.setTag(true); + tab.setIsNewTab(true); } } }); @@ -248,7 +249,7 @@ public void onClick(DialogInterface dialog, int which) { mShouldClose = true; LightningView tab = mTabsModel.lastTab(); if (tab != null) { - tab.setTag(true); + tab.setIsNewTab(true); } } } @@ -317,7 +318,7 @@ public synchronized void tabChanged(int position) { */ public synchronized boolean newTab(@Nullable String url, boolean show) { // Limit number of tabs for limited version of app - if (!Constants.FULL_VERSION && mTabsModel.size() >= 10) { + if (!BuildConfig.FULL_VERSION && mTabsModel.size() >= 10) { mView.showSnackbar(R.string.max_tabs); return false; } diff --git a/app/src/main/java/acr/browser/lightning/bus/BookmarkEvents.java b/app/src/main/java/acr/browser/lightning/bus/BookmarkEvents.java deleted file mode 100644 index a2f8ca3d2..000000000 --- a/app/src/main/java/acr/browser/lightning/bus/BookmarkEvents.java +++ /dev/null @@ -1,35 +0,0 @@ -package acr.browser.lightning.bus; - -import acr.browser.lightning.database.HistoryItem; - -public final class BookmarkEvents { - - private BookmarkEvents() { - // No instances - } - - /** - * The user ask to delete the selected bookmark - */ - public static class Deleted { - public final HistoryItem item; - - public Deleted(final HistoryItem item) { - this.item = item; - } - } - - /** - * Sended when a bookmark is edited - */ - public static class BookmarkChanged { - - public final HistoryItem oldBookmark; - public final HistoryItem newBookmark; - - public BookmarkChanged(final HistoryItem oldItem, final HistoryItem newItem) { - oldBookmark = oldItem; - newBookmark = newItem; - } - } -} diff --git a/app/src/main/java/acr/browser/lightning/bus/BrowserEvents.java b/app/src/main/java/acr/browser/lightning/bus/BrowserEvents.java deleted file mode 100644 index e0a281f58..000000000 --- a/app/src/main/java/acr/browser/lightning/bus/BrowserEvents.java +++ /dev/null @@ -1,37 +0,0 @@ -package acr.browser.lightning.bus; - -public final class BrowserEvents { - - private BrowserEvents() { - // No instances - } - - public final static class OpenHistoryInCurrentTab { - } - - /** - * The user ask to open the given url as new tab - */ - public final static class OpenUrlInNewTab { - - public enum Location { - NEW_TAB, - BACKGROUND, - INCOGNITO - } - - public final String url; - - public final Location location; - - public OpenUrlInNewTab(final String url) { - this.url = url; - this.location = Location.NEW_TAB; - } - - public OpenUrlInNewTab(final String url, Location location) { - this.url = url; - this.location = location; - } - } -} diff --git a/app/src/main/java/acr/browser/lightning/constant/BookmarkPage.java b/app/src/main/java/acr/browser/lightning/constant/BookmarkPage.java index c6d64ddf0..e4179329f 100644 --- a/app/src/main/java/acr/browser/lightning/constant/BookmarkPage.java +++ b/app/src/main/java/acr/browser/lightning/constant/BookmarkPage.java @@ -6,27 +6,32 @@ import android.app.Activity; import android.app.Application; import android.graphics.Bitmap; -import android.os.AsyncTask; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleOnSubscribe; +import com.anthonycr.bonsai.SingleSubscriber; + import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; -import java.lang.ref.WeakReference; import java.util.List; +import javax.inject.Inject; + import acr.browser.lightning.R; import acr.browser.lightning.app.BrowserApp; -import acr.browser.lightning.database.BookmarkManager; import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.database.bookmark.BookmarkModel; +import acr.browser.lightning.utils.Preconditions; import acr.browser.lightning.utils.ThemeUtils; import acr.browser.lightning.utils.Utils; -import acr.browser.lightning.view.LightningView; -public final class BookmarkPage extends AsyncTask { +public final class BookmarkPage { /** * The bookmark page standard suffix @@ -42,8 +47,14 @@ public final class BookmarkPage extends AsyncTask { private static final String HEADING_2 = "\n" + "\n" + - "\n" + + "\n" + "

"; private static final String PART1 = "
>() { + @Override + public void onItem(@Nullable List list) { + Preconditions.checkNonNull(list); + + final File bookmarkWebPage; + if (folder == null || folder.isEmpty()) { + bookmarkWebPage = new File(mFilesDir, FILENAME); + } else { + bookmarkWebPage = new File(mFilesDir, folder + '-' + FILENAME); + } + final StringBuilder bookmarkBuilder = new StringBuilder(HEADING_1 + mTitle + HEADING_2); + + final String folderIconPath = Constants.FILE + mCacheDir + '/' + FOLDER_ICON; + for (int n = 0, size = list.size(); n < size; n++) { + final HistoryItem item = list.get(n); + bookmarkBuilder.append(PART1); + if (item.isFolder()) { + final File folderPage = new File(mFilesDir, item.getTitle() + '-' + FILENAME); + bookmarkBuilder.append(Constants.FILE).append(folderPage); + bookmarkBuilder.append(PART2); + bookmarkBuilder.append(folderIconPath); + buildBookmarkPage(item.getTitle()); + } else { + bookmarkBuilder.append(item.getUrl()); + bookmarkBuilder.append(PART2).append(PART3); + bookmarkBuilder.append(item.getUrl()); + } + bookmarkBuilder.append(PART4); + bookmarkBuilder.append(item.getTitle()); + bookmarkBuilder.append(PART5); + } + bookmarkBuilder.append(END); + FileWriter bookWriter = null; + try { + //noinspection IOResourceOpenedButNotSafelyClosed + bookWriter = new FileWriter(bookmarkWebPage, false); + bookWriter.write(bookmarkBuilder.toString()); + } catch (IOException e) { + e.printStackTrace(); + } finally { + Utils.close(bookWriter); + } + } + }); } } diff --git a/app/src/main/java/acr/browser/lightning/constant/Constants.java b/app/src/main/java/acr/browser/lightning/constant/Constants.java index a6b47bd0a..731d4a5fd 100644 --- a/app/src/main/java/acr/browser/lightning/constant/Constants.java +++ b/app/src/main/java/acr/browser/lightning/constant/Constants.java @@ -8,15 +8,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import acr.browser.lightning.BuildConfig; - public final class Constants { private Constants() { } - public static final boolean FULL_VERSION = BuildConfig.FULL_VERSION; - // Hardcoded user agents public static final String DESKTOP_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36"; public static final String MOBILE_USER_AGENT = "Mozilla/5.0 (Linux; U; Android 4.4; en-us; Nexus 4 Build/JOP24G) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30"; @@ -67,13 +63,10 @@ private Constants() { public static final String ABOUT = "about:"; public static final String FOLDER = "folder://"; - // Application log tag - public static final String TAG = "Lightning"; - // These should match the order of @array/proxy_choices_array @IntDef({NO_PROXY, PROXY_ORBOT, PROXY_I2P, PROXY_MANUAL}) @Retention(RetentionPolicy.SOURCE) - public @interface PROXY {} + public @interface Proxy {} public static final int NO_PROXY = 0; public static final int PROXY_ORBOT = 1; diff --git a/app/src/main/java/acr/browser/lightning/constant/DownloadsPage.java b/app/src/main/java/acr/browser/lightning/constant/DownloadsPage.java new file mode 100644 index 000000000..5e5eb8ad7 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/constant/DownloadsPage.java @@ -0,0 +1,134 @@ +/* + * Copyright 2014 A.C.R. Development + */ +package acr.browser.lightning.constant; + +import android.app.Application; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleOnSubscribe; +import com.anthonycr.bonsai.SingleSubscriber; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; + +import javax.inject.Inject; + +import acr.browser.lightning.R; +import acr.browser.lightning.app.BrowserApp; +import acr.browser.lightning.database.downloads.DownloadItem; +import acr.browser.lightning.database.downloads.DownloadsModel; +import acr.browser.lightning.preference.PreferenceManager; +import acr.browser.lightning.utils.Preconditions; +import acr.browser.lightning.utils.Utils; + +public final class DownloadsPage { + + /** + * The download page standard suffix + */ + public static final String FILENAME = "downloads.html"; + + private static final String HEADING_1 = "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + ""; + + private static final String HEADING_2 = "" + + "" + + "" + + "
"; + + private static final String PART1 = "

"; + + private static final String PART3 = "

"; + + private static final String PART4 = "

"; + + private static final String END = "
"; + + private File mFilesDir; + + @Inject Application mApp; + @Inject PreferenceManager mPreferenceManager; + @Inject DownloadsModel mManager; + + @NonNull private final String mTitle; + + public DownloadsPage() { + BrowserApp.getAppComponent().inject(this); + mTitle = mApp.getString(R.string.action_downloads); + } + + @NonNull + public Single getDownloadsPage() { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + mFilesDir = mApp.getFilesDir(); + + buildDownloadsPage(); + + File downloadsWebPage = new File(mFilesDir, FILENAME); + + subscriber.onItem(Constants.FILE + downloadsWebPage); + subscriber.onComplete(); + } + }); + } + + private void buildDownloadsPage() { + mManager.getAllDownloads() + .subscribe(new SingleOnSubscribe>() { + @Override + public void onItem(@Nullable List list) { + Preconditions.checkNonNull(list); + String directory = mPreferenceManager.getDownloadDirectory(); + + final StringBuilder downloadsBuilder = new StringBuilder(HEADING_1 + mTitle + HEADING_2); + + for (int n = 0, size = list.size(); n < size; n++) { + final DownloadItem item = list.get(n); + downloadsBuilder.append(PART1); + downloadsBuilder.append("file://"); + downloadsBuilder.append(directory); + downloadsBuilder.append("/"); + downloadsBuilder.append(item.getTitle()); + downloadsBuilder.append(PART2); + downloadsBuilder.append(item.getTitle()); + + if (!item.getContentSize().isEmpty()) { + downloadsBuilder.append(" ["); + downloadsBuilder.append(item.getContentSize()); + downloadsBuilder.append("]"); + } + + downloadsBuilder.append(PART3); + downloadsBuilder.append(item.getUrl()); + downloadsBuilder.append(PART4); + } + downloadsBuilder.append(END); + FileWriter bookWriter = null; + try { + //noinspection IOResourceOpenedButNotSafelyClosed + bookWriter = new FileWriter(new File(mFilesDir, FILENAME), false); + bookWriter.write(downloadsBuilder.toString()); + } catch (IOException e) { + e.printStackTrace(); + } finally { + Utils.close(bookWriter); + } + } + }); + } + +} diff --git a/app/src/main/java/acr/browser/lightning/constant/HistoryPage.java b/app/src/main/java/acr/browser/lightning/constant/HistoryPage.java index 44c3ca8f3..11143fcea 100644 --- a/app/src/main/java/acr/browser/lightning/constant/HistoryPage.java +++ b/app/src/main/java/acr/browser/lightning/constant/HistoryPage.java @@ -4,123 +4,140 @@ package acr.browser.lightning.constant; import android.app.Application; -import android.os.AsyncTask; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableAction; +import com.anthonycr.bonsai.CompletableSubscriber; +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleOnSubscribe; +import com.anthonycr.bonsai.SingleSubscriber; + import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.lang.ref.WeakReference; import java.util.Iterator; import java.util.List; +import javax.inject.Inject; + import acr.browser.lightning.R; import acr.browser.lightning.app.BrowserApp; -import acr.browser.lightning.database.HistoryDatabase; import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.database.history.HistoryModel; +import acr.browser.lightning.utils.Preconditions; import acr.browser.lightning.utils.Utils; -import acr.browser.lightning.view.LightningView; -public class HistoryPage extends AsyncTask { +public class HistoryPage { - private static final String TAG = HistoryPage.class.getSimpleName(); + private static final String TAG = "HistoryPage"; public static final String FILENAME = "history.html"; - private static final String HEADING_1 = ""; + private static final String HEADING_1 = "<!DOCTYPE html><html xmlns=http://www.w3.org/1999/xhtml>\n" + + "<head>\n" + + "<meta content=en-us http-equiv=Content-Language />\n" + + "<meta content='text/html; charset=utf-8' http-equiv=Content-Type />\n" + + "<meta name=viewport content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'>\n" + + "<title>"; - private static final String HEADING_2 = "
"; + private static final String HEADING_2 = "
"; - private static final String PART1 = "

"; + private static final String PART2 = "'>

"; - private static final String PART3 = "

"; + private static final String PART3 = "

"; - private static final String PART4 = "

"; + private static final String PART4 = "

"; private static final String END = "
"; - @NonNull private final WeakReference mTabReference; - @NonNull private final Application mApp; @NonNull private final String mTitle; - private final HistoryDatabase mHistoryDatabase; - @Nullable private String mHistoryUrl = null; + @Inject Application mApp; - public HistoryPage(LightningView tab, @NonNull Application app, HistoryDatabase database) { - mTabReference = new WeakReference<>(tab); - mApp = app; - mTitle = app.getString(R.string.action_history); - mHistoryDatabase = database; - } - - @Nullable - @Override - protected Void doInBackground(Void... params) { - mHistoryUrl = getHistoryPage(); - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - super.onPostExecute(aVoid); - LightningView tab = mTabReference.get(); - if (tab != null && mHistoryUrl != null) { - tab.loadUrl(mHistoryUrl); - } + public HistoryPage() { + BrowserApp.getAppComponent().inject(this); + mTitle = mApp.getString(R.string.action_history); } @NonNull - private String getHistoryPage() { - StringBuilder historyBuilder = new StringBuilder(HEADING_1 + mTitle + HEADING_2); - List historyList = mHistoryDatabase.getLastHundredItems(); - Iterator it = historyList.iterator(); - HistoryItem helper; - while (it.hasNext()) { - helper = it.next(); - historyBuilder.append(PART1); - historyBuilder.append(helper.getUrl()); - historyBuilder.append(PART2); - historyBuilder.append(helper.getTitle()); - historyBuilder.append(PART3); - historyBuilder.append(helper.getUrl()); - historyBuilder.append(PART4); - } - - historyBuilder.append(END); - File historyWebPage = new File(mApp.getFilesDir(), FILENAME); - FileWriter historyWriter = null; - try { - //noinspection IOResourceOpenedButNotSafelyClosed - historyWriter = new FileWriter(historyWebPage, false); - historyWriter.write(historyBuilder.toString()); - } catch (IOException e) { - Log.e(TAG, "Unable to write history page to disk", e); - } finally { - Utils.close(historyWriter); - } - return Constants.FILE + historyWebPage; - } - - public void load() { - executeOnExecutor(BrowserApp.getIOThread()); + public Single getHistoryPage() { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull final SingleSubscriber subscriber) { + final StringBuilder historyBuilder = new StringBuilder(HEADING_1 + mTitle + HEADING_2); + + HistoryModel.lastHundredVisitedHistoryItems() + .subscribe(new SingleOnSubscribe>() { + @Override + public void onItem(@Nullable List item) { + + Preconditions.checkNonNull(item); + Iterator it = item.iterator(); + HistoryItem helper; + while (it.hasNext()) { + helper = it.next(); + historyBuilder.append(PART1); + historyBuilder.append(helper.getUrl()); + historyBuilder.append(PART2); + historyBuilder.append(helper.getTitle()); + historyBuilder.append(PART3); + historyBuilder.append(helper.getUrl()); + historyBuilder.append(PART4); + } + + historyBuilder.append(END); + File historyWebPage = new File(mApp.getFilesDir(), FILENAME); + FileWriter historyWriter = null; + try { + //noinspection IOResourceOpenedButNotSafelyClosed + historyWriter = new FileWriter(historyWebPage, false); + historyWriter.write(historyBuilder.toString()); + } catch (IOException e) { + Log.e(TAG, "Unable to write history page to disk", e); + } finally { + Utils.close(historyWriter); + } + + subscriber.onItem(Constants.FILE + historyWebPage); + subscriber.onComplete(); + } + }); + } + }); } /** - * Use this method to immediately delete the history - * page on the current thread. This will clear the - * cached history page that was stored on file. + * Use this observable to immediately delete the history + * page. This will clear the cached history page that was + * stored on file. * - * @param application the application object needed to get the file. + * @return a completable that deletes the history page + * when subscribed. */ - public static void deleteHistoryPage(@NonNull Application application) { - File historyWebPage = new File(application.getFilesDir(), FILENAME); - if (historyWebPage.exists()) { - historyWebPage.delete(); - } + @NonNull + public static Completable deleteHistoryPage(@NonNull final Application application) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + File historyWebPage = new File(application.getFilesDir(), FILENAME); + if (historyWebPage.exists()) { + historyWebPage.delete(); + } + + subscriber.onComplete(); + } + }); } } \ No newline at end of file diff --git a/app/src/main/java/acr/browser/lightning/constant/StartPage.java b/app/src/main/java/acr/browser/lightning/constant/StartPage.java index abccc832c..b422ab163 100644 --- a/app/src/main/java/acr/browser/lightning/constant/StartPage.java +++ b/app/src/main/java/acr/browser/lightning/constant/StartPage.java @@ -4,14 +4,15 @@ package acr.browser.lightning.constant; import android.app.Application; -import android.os.AsyncTask; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; + +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleSubscriber; import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.lang.ref.WeakReference; import javax.inject.Inject; @@ -19,9 +20,8 @@ import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.preference.PreferenceManager; import acr.browser.lightning.utils.Utils; -import acr.browser.lightning.view.LightningView; -public class StartPage extends AsyncTask { +public class StartPage { public static final String FILENAME = "homepage.html"; @@ -55,142 +55,118 @@ public class StartPage extends AsyncTask { private static final String END = "\" + document.getElementById(\"search_input\").value;document.getElementById(\"search_input\").value = \"\";}return false;}"; @NonNull private final String mTitle; - @NonNull private final Application mApp; - @NonNull private final WeakReference mTabReference; + @Inject Application mApp; @Inject PreferenceManager mPreferenceManager; - private String mStartpageUrl; - - public StartPage(LightningView tab, @NonNull Application app) { + public StartPage() { BrowserApp.getAppComponent().inject(this); - mTitle = app.getString(R.string.home); - mApp = app; - mTabReference = new WeakReference<>(tab); - } - - @Nullable - @Override - protected Void doInBackground(Void... params) { - mStartpageUrl = getHomepage(); - return null; + mTitle = mApp.getString(R.string.home); } - @Override - protected void onPostExecute(Void aVoid) { - super.onPostExecute(aVoid); - LightningView tab = mTabReference.get(); - if (tab != null) { - tab.loadUrl(mStartpageUrl); - } - } - - /** - * This method builds the homepage and returns the local URL to be loaded - * when it finishes building. - * - * @return the URL to load - */ @NonNull - private String getHomepage() { - StringBuilder homepageBuilder = new StringBuilder(HEAD_1 + mTitle + HEAD_2); - String icon; - String searchUrl; - switch (mPreferenceManager.getSearchChoice()) { - case 0: - // CUSTOM SEARCH - icon = "file:///android_asset/lightning.png"; - searchUrl = mPreferenceManager.getSearchUrl(); - break; - case 1: - // GOOGLE_SEARCH; - icon = "file:///android_asset/google.png"; - // "https://www.google.com/images/srpr/logo11w.png"; - searchUrl = Constants.GOOGLE_SEARCH; - break; - case 2: - // ANDROID SEARCH; - icon = "file:///android_asset/ask.png"; - searchUrl = Constants.ASK_SEARCH; - break; - case 3: - // BING_SEARCH; - icon = "file:///android_asset/bing.png"; - // "http://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/Bing_logo_%282013%29.svg/500px-Bing_logo_%282013%29.svg.png"; - searchUrl = Constants.BING_SEARCH; - break; - case 4: - // YAHOO_SEARCH; - icon = "file:///android_asset/yahoo.png"; - // "http://upload.wikimedia.org/wikipedia/commons/thumb/2/24/Yahoo%21_logo.svg/799px-Yahoo%21_logo.svg.png"; - searchUrl = Constants.YAHOO_SEARCH; - break; - case 5: - // STARTPAGE_SEARCH; - icon = "file:///android_asset/png"; - // "https://com/graphics/startp_logo.gif"; - searchUrl = Constants.STARTPAGE_SEARCH; - break; - case 6: - // STARTPAGE_MOBILE - icon = "file:///android_asset/png"; - // "https://com/graphics/startp_logo.gif"; - searchUrl = Constants.STARTPAGE_MOBILE_SEARCH; - break; - case 7: - // DUCK_SEARCH; - icon = "file:///android_asset/duckduckgo.png"; - // "https://duckduckgo.com/assets/logo_homepage.normal.v101.png"; - searchUrl = Constants.DUCK_SEARCH; - break; - case 8: - // DUCK_LITE_SEARCH; - icon = "file:///android_asset/duckduckgo.png"; - // "https://duckduckgo.com/assets/logo_homepage.normal.v101.png"; - searchUrl = Constants.DUCK_LITE_SEARCH; - break; - case 9: - // BAIDU_SEARCH; - icon = "file:///android_asset/baidu.png"; - // "http://www.baidu.com/img/bdlogo.gif"; - searchUrl = Constants.BAIDU_SEARCH; - break; - case 10: - // YANDEX_SEARCH; - icon = "file:///android_asset/yandex.png"; - // "http://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Yandex.svg/600px-Yandex.svg.png"; - searchUrl = Constants.YANDEX_SEARCH; - break; - default: - // DEFAULT GOOGLE_SEARCH; - icon = "file:///android_asset/google.png"; - searchUrl = Constants.GOOGLE_SEARCH; - break; - - } - - homepageBuilder.append(icon); - homepageBuilder.append(MIDDLE); - homepageBuilder.append(searchUrl); - homepageBuilder.append(END); - - File homepage = new File(mApp.getFilesDir(), FILENAME); - FileWriter hWriter = null; - try { - //noinspection IOResourceOpenedButNotSafelyClosed - hWriter = new FileWriter(homepage, false); - hWriter.write(homepageBuilder.toString()); - } catch (IOException e) { - e.printStackTrace(); - } finally { - Utils.close(hWriter); - } - - return Constants.FILE + homepage; - } - - public void load() { - executeOnExecutor(BrowserApp.getIOThread()); + public Single getHomepage() { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + + StringBuilder homepageBuilder = new StringBuilder(HEAD_1 + mTitle + HEAD_2); + String icon; + String searchUrl; + switch (mPreferenceManager.getSearchChoice()) { + case 0: + // CUSTOM SEARCH + icon = "file:///android_asset/lightning.png"; + searchUrl = mPreferenceManager.getSearchUrl(); + break; + case 1: + // GOOGLE_SEARCH; + icon = "file:///android_asset/google.png"; + // "https://www.google.com/images/srpr/logo11w.png"; + searchUrl = Constants.GOOGLE_SEARCH; + break; + case 2: + // ANDROID SEARCH; + icon = "file:///android_asset/ask.png"; + searchUrl = Constants.ASK_SEARCH; + break; + case 3: + // BING_SEARCH; + icon = "file:///android_asset/bing.png"; + // "http://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/Bing_logo_%282013%29.svg/500px-Bing_logo_%282013%29.svg.png"; + searchUrl = Constants.BING_SEARCH; + break; + case 4: + // YAHOO_SEARCH; + icon = "file:///android_asset/yahoo.png"; + // "http://upload.wikimedia.org/wikipedia/commons/thumb/2/24/Yahoo%21_logo.svg/799px-Yahoo%21_logo.svg.png"; + searchUrl = Constants.YAHOO_SEARCH; + break; + case 5: + // STARTPAGE_SEARCH; + icon = "file:///android_asset/startpage.png"; + // "https://com/graphics/startp_logo.gif"; + searchUrl = Constants.STARTPAGE_SEARCH; + break; + case 6: + // STARTPAGE_MOBILE + icon = "file:///android_asset/startpage.png"; + // "https://com/graphics/startp_logo.gif"; + searchUrl = Constants.STARTPAGE_MOBILE_SEARCH; + break; + case 7: + // DUCK_SEARCH; + icon = "file:///android_asset/duckduckgo.png"; + // "https://duckduckgo.com/assets/logo_homepage.normal.v101.png"; + searchUrl = Constants.DUCK_SEARCH; + break; + case 8: + // DUCK_LITE_SEARCH; + icon = "file:///android_asset/duckduckgo.png"; + // "https://duckduckgo.com/assets/logo_homepage.normal.v101.png"; + searchUrl = Constants.DUCK_LITE_SEARCH; + break; + case 9: + // BAIDU_SEARCH; + icon = "file:///android_asset/baidu.png"; + // "http://www.baidu.com/img/bdlogo.gif"; + searchUrl = Constants.BAIDU_SEARCH; + break; + case 10: + // YANDEX_SEARCH; + icon = "file:///android_asset/yandex.png"; + // "http://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Yandex.svg/600px-Yandex.svg.png"; + searchUrl = Constants.YANDEX_SEARCH; + break; + default: + // DEFAULT GOOGLE_SEARCH; + icon = "file:///android_asset/google.png"; + searchUrl = Constants.GOOGLE_SEARCH; + break; + + } + + homepageBuilder.append(icon); + homepageBuilder.append(MIDDLE); + homepageBuilder.append(searchUrl); + homepageBuilder.append(END); + + File homepage = new File(mApp.getFilesDir(), FILENAME); + FileWriter hWriter = null; + try { + //noinspection IOResourceOpenedButNotSafelyClosed + hWriter = new FileWriter(homepage, false); + hWriter.write(homepageBuilder.toString()); + } catch (IOException e) { + e.printStackTrace(); + } finally { + Utils.close(hWriter); + } + + subscriber.onItem(Constants.FILE + homepage); + + } + }); } } diff --git a/app/src/main/java/acr/browser/lightning/controller/UIController.java b/app/src/main/java/acr/browser/lightning/controller/UIController.java index 7f2bb2bb3..a9021a715 100644 --- a/app/src/main/java/acr/browser/lightning/controller/UIController.java +++ b/app/src/main/java/acr/browser/lightning/controller/UIController.java @@ -16,6 +16,7 @@ import acr.browser.lightning.activity.TabsManager; import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.dialog.LightningDialogBuilder; import acr.browser.lightning.view.LightningView; public interface UIController { @@ -83,4 +84,14 @@ public interface UIController { void onHomeButtonPressed(); + void handleBookmarksChange(); + + void handleDownloadDeleted(); + + void handleBookmarkDeleted(@NonNull HistoryItem item); + + void handleNewTab(@NonNull LightningDialogBuilder.NewTab newTabType, @NonNull String url); + + void handleHistoryChange(); + } diff --git a/app/src/main/java/acr/browser/lightning/database/BookmarkManager.java b/app/src/main/java/acr/browser/lightning/database/BookmarkManager.java deleted file mode 100644 index e8e48ca6b..000000000 --- a/app/src/main/java/acr/browser/lightning/database/BookmarkManager.java +++ /dev/null @@ -1,558 +0,0 @@ -package acr.browser.lightning.database; - -import android.app.Activity; -import android.content.Context; -import android.os.Environment; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import acr.browser.lightning.R; -import acr.browser.lightning.constant.Constants; -import acr.browser.lightning.utils.Utils; - -@Singleton -public class BookmarkManager { - - private static final String TAG = BookmarkManager.class.getSimpleName(); - - private static final String TITLE = "title"; - private static final String URL = "url"; - private static final String FOLDER = "folder"; - private static final String ORDER = "order"; - private static final String FILE_BOOKMARKS = "bookmarks.dat"; - - @NonNull private final String DEFAULT_BOOKMARK_TITLE; - - private Map mBookmarksMap; - @NonNull private String mCurrentFolder = ""; - @NonNull private final ExecutorService mExecutor; - private File mFilesDir; - - @Inject - public BookmarkManager(@NonNull Context context) { - mExecutor = Executors.newSingleThreadExecutor(); - DEFAULT_BOOKMARK_TITLE = context.getString(R.string.untitled); - mExecutor.execute(new BookmarkInitializer(context)); - } - - /** - * Look for bookmark using the url - * - * @param url the lookup url - * @return the bookmark as an {@link HistoryItem} or null - */ - @Nullable - public HistoryItem findBookmarkForUrl(final String url) { - return mBookmarksMap.get(url); - } - - /** - * Initialize the BookmarkManager, it's a one-time operation and will be executed asynchronously. - * When done, mReady flag will been set to true. - */ - private class BookmarkInitializer implements Runnable { - private final Context mContext; - - BookmarkInitializer(Context context) { - mContext = context; - } - - @Override - public void run() { - synchronized (BookmarkManager.this) { - mFilesDir = mContext.getFilesDir(); - final Map bookmarks = new HashMap<>(); - final File bookmarksFile = new File(mFilesDir, FILE_BOOKMARKS); - - BufferedReader bookmarksReader = null; - InputStream inputStream = null; - try { - if (bookmarksFile.exists() && bookmarksFile.isFile()) { - //noinspection IOResourceOpenedButNotSafelyClosed - inputStream = new FileInputStream(bookmarksFile); - } else { - inputStream = mContext.getResources().openRawResource(R.raw.default_bookmarks); - } - //noinspection IOResourceOpenedButNotSafelyClosed - bookmarksReader = new BufferedReader(new InputStreamReader(inputStream)); - String line; - while ((line = bookmarksReader.readLine()) != null) { - try { - JSONObject object = new JSONObject(line); - HistoryItem item = new HistoryItem(); - item.setTitle(object.getString(TITLE)); - final String url = object.getString(URL); - item.setUrl(url); - item.setFolder(object.getString(FOLDER)); - item.setOrder(object.getInt(ORDER)); - item.setImageId(R.drawable.ic_bookmark); - bookmarks.put(url, item); - } catch (JSONException e) { - Log.e(TAG, "Can't parse line " + line, e); - } - } - } catch (IOException e) { - Log.e(TAG, "Error reading the bookmarks file", e); - } finally { - Utils.close(bookmarksReader); - Utils.close(inputStream); - } - mBookmarksMap = bookmarks; - } - } - - } - - /** - * Dump all the given bookmarks to the bookmark file using a temporary file - */ - private class BookmarksWriter implements Runnable { - - private final List mBookmarks; - - BookmarksWriter(List bookmarks) { - mBookmarks = bookmarks; - } - - @Override - public void run() { - final File tempFile = new File(mFilesDir, - String.format(Locale.US, "bm_%d.dat", System.currentTimeMillis())); - final File bookmarksFile = new File(mFilesDir, FILE_BOOKMARKS); - boolean success = false; - BufferedWriter bookmarkWriter = null; - try { - //noinspection IOResourceOpenedButNotSafelyClosed - bookmarkWriter = new BufferedWriter(new FileWriter(tempFile, false)); - JSONObject object = new JSONObject(); - for (HistoryItem item : mBookmarks) { - object.put(TITLE, item.getTitle()); - object.put(URL, item.getUrl()); - object.put(FOLDER, item.getFolder()); - object.put(ORDER, item.getOrder()); - bookmarkWriter.write(object.toString()); - bookmarkWriter.newLine(); - } - success = true; - } catch (@NonNull IOException | JSONException e) { - e.printStackTrace(); - } finally { - Utils.close(bookmarkWriter); - } - - if (success) { - // Overwrite the bookmarks file by renaming the temp file - //noinspection ResultOfMethodCallIgnored - tempFile.renameTo(bookmarksFile); - } - } - } - - @Override - protected void finalize() throws Throwable { - mExecutor.shutdownNow(); - super.finalize(); - } - - public boolean isBookmark(String url) { - return mBookmarksMap.containsKey(url); - } - - /** - * This method adds the the HistoryItem item to permanent bookmark storage.
- * This operation is blocking if the manager is still not ready. - * - * @param item the item to add - * @return It returns true if the operation was successful. - */ - public synchronized boolean addBookmark(@NonNull HistoryItem item) { - final String url = item.getUrl(); - if (mBookmarksMap.containsKey(url)) { - return false; - } - mBookmarksMap.put(url, item); - mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values()))); - return true; - } - - /** - * This method adds the list of HistoryItems to permanent bookmark storage - * - * @param list the list of HistoryItems to add to bookmarks - */ - public synchronized void addBookmarkList(@Nullable List list) { - if (list == null || list.isEmpty()) { - return; - } - for (HistoryItem item : list) { - final String url = item.getUrl(); - if (!mBookmarksMap.containsKey(url)) { - mBookmarksMap.put(url, item); - } - } - mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values()))); - } - - /** - * This method deletes the bookmark with the given url. It returns - * true if the deletion was successful. - * - * @param deleteItem the bookmark item to delete - */ - public synchronized boolean deleteBookmark(@Nullable HistoryItem deleteItem) { - if (deleteItem == null || deleteItem.isFolder()) { - return false; - } - mBookmarksMap.remove(deleteItem.getUrl()); - mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values()))); - return true; - } - - /** - * renames a folder and moves all it's contents to that folder - * - * @param oldName the folder to be renamed - * @param newName the new name of the folder - */ - public synchronized void renameFolder(@NonNull String oldName, @NonNull String newName) { - if (newName.isEmpty()) { - return; - } - for (HistoryItem item : mBookmarksMap.values()) { - if (item.getFolder().equals(oldName)) { - item.setFolder(newName); - } else if (item.isFolder() && item.getTitle().equals(oldName)) { - item.setTitle(newName); - item.setUrl(Constants.FOLDER + newName); - } - } - mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values()))); - } - - /** - * Delete the folder and move all bookmarks to the top level - * - * @param name the name of the folder to be deleted - */ - public synchronized void deleteFolder(@NonNull String name) { - final Map bookmarks = new HashMap<>(); - for (HistoryItem item : mBookmarksMap.values()) { - final String url = item.getUrl(); - if (item.isFolder()) { - if (!item.getTitle().equals(name)) { - bookmarks.put(url, item); - } - } else { - if (item.getFolder().equals(name)) { - item.setFolder(""); - } - bookmarks.put(url, item); - } - } - mBookmarksMap = bookmarks; - mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values()))); - } - - /** - * This method deletes ALL bookmarks created - * by the user. Use this method carefully and - * do not use it without explicit user consent. - */ - public synchronized void deleteAllBookmarks() { - mBookmarksMap = new HashMap<>(); - mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values()))); - } - - /** - * This method edits a particular bookmark in the bookmark database - * - * @param oldItem This is the old item that you wish to edit - * @param newItem This is the new item that will overwrite the old item - */ - public synchronized void editBookmark(@Nullable HistoryItem oldItem, @Nullable HistoryItem newItem) { - if (oldItem == null || newItem == null || oldItem.isFolder()) { - return; - } - if (newItem.getUrl().isEmpty()) { - deleteBookmark(oldItem); - return; - } - if (newItem.getTitle().isEmpty()) { - newItem.setTitle(DEFAULT_BOOKMARK_TITLE); - } - final String oldUrl = oldItem.getUrl(); - final String newUrl = newItem.getUrl(); - if (!oldUrl.equals(newUrl)) { - // The url has been changed, remove the old one - mBookmarksMap.remove(oldUrl); - } - mBookmarksMap.put(newUrl, newItem); - mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values()))); - } - - /** - * This method exports the stored bookmarks to a text file in the device's - * external download directory - */ - public synchronized void exportBookmarks(@NonNull Activity activity) { - List bookmarkList = getAllBookmarks(true); - File bookmarksExport = new File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - "BookmarksExport.txt"); - int counter = 0; - while (bookmarksExport.exists()) { - counter++; - bookmarksExport = new File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - "BookmarksExport-" + counter + ".txt"); - } - BufferedWriter bookmarkWriter = null; - try { - //noinspection IOResourceOpenedButNotSafelyClosed - bookmarkWriter = new BufferedWriter(new FileWriter(bookmarksExport, - false)); - JSONObject object = new JSONObject(); - for (HistoryItem item : bookmarkList) { - object.put(TITLE, item.getTitle()); - object.put(URL, item.getUrl()); - object.put(FOLDER, item.getFolder()); - object.put(ORDER, item.getOrder()); - bookmarkWriter.write(object.toString()); - bookmarkWriter.newLine(); - } - Utils.showSnackbar(activity, activity.getString(R.string.bookmark_export_path) - + ' ' + bookmarksExport.getPath()); - } catch (@NonNull IOException | JSONException e) { - e.printStackTrace(); - } finally { - Utils.close(bookmarkWriter); - } - - } - - /** - * This method returns a list of ALL stored bookmarks. - * This is a disk-bound operation and should not be - * done very frequently. - * - * @param sort force to sort the returned bookmarkList - * @return returns a list of bookmarks that can be sorted - */ - @NonNull - public synchronized List getAllBookmarks(boolean sort) { - final List bookmarks = new ArrayList<>(mBookmarksMap.values()); - if (sort) { - Collections.sort(bookmarks, new SortIgnoreCase()); - } - return bookmarks; - } - - /** - * This method returns a list of bookmarks and folders located in the specified folder. - * This method should generally be used by the UI when it needs a list to display to the - * user as it returns a subset of all bookmarks and includes folders as well which are - * really 'fake' bookmarks. - * - * @param folder the name of the folder to retrieve bookmarks from - * @return a list of bookmarks found in that folder - */ - @NonNull - public synchronized List getBookmarksFromFolder(@Nullable String folder, boolean sort) { - List bookmarks = new ArrayList<>(1); - if (folder == null || folder.isEmpty()) { - bookmarks.addAll(getFolders(sort)); - folder = ""; - } - mCurrentFolder = folder; - for (HistoryItem item : mBookmarksMap.values()) { - if (item.getFolder().equals(folder)) - bookmarks.add(item); - } - if (sort) { - Collections.sort(bookmarks, new SortIgnoreCase()); - } - return bookmarks; - } - - /** - * Different from {@link #getBookmarksFromFolder(String, boolean)} only in - * that it doesn't affect the internal state of the bookmark manager which - * tracks the current folder used by the bookmark drawer. - *

- * This method returns a list of bookmarks and folders located in the specified folder. - * This method should generally be used by the UI when it needs a list to display to the - * user as it returns a subset of all bookmarks and includes folders as well which are - * really 'fake' bookmarks. - * - * @param folder the name of the folder to retrieve bookmarks from - * @return a list of bookmarks found in that folder - */ - @NonNull - public synchronized List getBookmarksCopyFromFolder(@Nullable String folder, boolean sort) { - List bookmarks = new ArrayList<>(1); - if (folder == null || folder.isEmpty()) { - bookmarks.addAll(getFolders(sort)); - folder = ""; - } - for (HistoryItem item : mBookmarksMap.values()) { - if (item.getFolder().equals(folder)) - bookmarks.add(item); - } - if (sort) { - Collections.sort(bookmarks, new SortIgnoreCase()); - } - return bookmarks; - } - - /** - * Tells you if you are at the root folder or in a subfolder - * - * @return returns true if you are in the root folder - */ - public boolean isRootFolder() { - return mCurrentFolder.isEmpty(); - } - - /** - * Returns the current folder - * - * @return the current folder - */ - @Nullable - public String getCurrentFolder() { - return mCurrentFolder; - } - - /** - * This method returns a list of all folders. - * Folders cannot be empty as they are generated from - * the list of bookmarks that have non-empty folder fields. - * - * @return a list of all folders - */ - @NonNull - private synchronized List getFolders(boolean sort) { - final HashMap folders = new HashMap<>(); - for (HistoryItem item : mBookmarksMap.values()) { - final String folderName = item.getFolder(); - if (!folderName.isEmpty() && !folders.containsKey(folderName)) { - final HistoryItem folder = new HistoryItem(); - folder.setIsFolder(true); - folder.setTitle(folderName); - folder.setImageId(R.drawable.ic_folder); - folder.setUrl(Constants.FOLDER + folderName); - folders.put(folderName, folder); - } - } - final List result = new ArrayList<>(folders.values()); - if (sort) { - Collections.sort(result, new SortIgnoreCase()); - } - return result; - } - - /** - * returns a list of folder titles that can be used for suggestions in a - * simple list adapter - * - * @return a list of folder title strings - */ - @NonNull - public synchronized List getFolderTitles() { - final Set folders = new HashSet<>(); - for (HistoryItem item : mBookmarksMap.values()) { - final String folderName = item.getFolder(); - if (!folderName.isEmpty()) { - folders.add(folderName); - } - } - return new ArrayList<>(folders); - } - - /** - * This method imports the bookmarks from a backup file that is located on - * external storage - * - * @param file the file to attempt to import bookmarks from - */ - public synchronized void importBookmarksFromFile(@Nullable File file, @NonNull Activity activity) { - if (file == null) { - return; - } - List list = new ArrayList<>(); - BufferedReader bookmarksReader = null; - try { - //noinspection IOResourceOpenedButNotSafelyClosed - bookmarksReader = new BufferedReader(new FileReader(file)); - String line; - int number = 0; - while ((line = bookmarksReader.readLine()) != null) { - JSONObject object = new JSONObject(line); - HistoryItem item = new HistoryItem(); - item.setTitle(object.getString(TITLE)); - item.setUrl(object.getString(URL)); - item.setFolder(object.getString(FOLDER)); - item.setOrder(object.getInt(ORDER)); - list.add(item); - number++; - } - addBookmarkList(list); - String message = activity.getResources().getString(R.string.message_import); - Utils.showSnackbar(activity, number + " " + message); - } catch (@NonNull IOException | JSONException e) { - e.printStackTrace(); - Utils.createInformativeDialog(activity, R.string.title_error, R.string.import_bookmark_error); - } finally { - Utils.close(bookmarksReader); - } - } - - /** - * This class sorts bookmarks alphabetically, with folders coming after bookmarks - */ - private static class SortIgnoreCase implements Comparator { - - public int compare(@Nullable HistoryItem o1, @Nullable HistoryItem o2) { - if (o1 == null || o2 == null) { - return 0; - } - if (o1.isFolder() == o2.isFolder()) { - return o1.getTitle().toLowerCase(Locale.getDefault()) - .compareTo(o2.getTitle().toLowerCase(Locale.getDefault())); - - } else { - return o1.isFolder() ? 1 : -1; - } - } - - } -} diff --git a/app/src/main/java/acr/browser/lightning/database/HistoryDatabase.java b/app/src/main/java/acr/browser/lightning/database/HistoryDatabase.java deleted file mode 100644 index 64d7327fe..000000000 --- a/app/src/main/java/acr/browser/lightning/database/HistoryDatabase.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright 2014 A.C.R. Development - */ -package acr.browser.lightning.database; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import acr.browser.lightning.R; -import acr.browser.lightning.app.BrowserApp; - -@Singleton -public class HistoryDatabase extends SQLiteOpenHelper { - - // All Static variables - // Database Version - private static final int DATABASE_VERSION = 2; - - // Database Name - private static final String DATABASE_NAME = "historyManager"; - - // HistoryItems table name - private static final String TABLE_HISTORY = "history"; - - // HistoryItems Table Columns names - private static final String KEY_ID = "id"; - private static final String KEY_URL = "url"; - private static final String KEY_TITLE = "title"; - private static final String KEY_TIME_VISITED = "time"; - - @Nullable private SQLiteDatabase mDatabase; - - @Inject - public HistoryDatabase(@NonNull Context context) { - super(context.getApplicationContext(), DATABASE_NAME, null, DATABASE_VERSION); - initialize(); - } - - private void initialize() { - BrowserApp.getTaskThread().execute(new Runnable() { - @Override - public void run() { - synchronized (HistoryDatabase.this) { - mDatabase = HistoryDatabase.this.getWritableDatabase(); - } - } - }); - } - - // Creating Tables - @Override - public void onCreate(@NonNull SQLiteDatabase db) { - String CREATE_HISTORY_TABLE = "CREATE TABLE " + TABLE_HISTORY + '(' + KEY_ID - + " INTEGER PRIMARY KEY," + KEY_URL + " TEXT," + KEY_TITLE + " TEXT," - + KEY_TIME_VISITED + " INTEGER" + ')'; - db.execSQL(CREATE_HISTORY_TABLE); - } - - // Upgrading database - @Override - public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { - // Drop older table if it exists - db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY); - // Create tables again - onCreate(db); - } - - public synchronized void deleteHistory() { - mDatabase = openIfNecessary(); - mDatabase.delete(TABLE_HISTORY, null, null); - mDatabase.close(); - mDatabase = this.getWritableDatabase(); - } - - @Override - public synchronized void close() { - if (mDatabase != null) { - mDatabase.close(); - mDatabase = null; - } - super.close(); - } - - @NonNull - private SQLiteDatabase openIfNecessary() { - if (mDatabase == null || !mDatabase.isOpen()) { - mDatabase = this.getWritableDatabase(); - } - return mDatabase; - } - - public synchronized void deleteHistoryItem(@NonNull String url) { - mDatabase = openIfNecessary(); - mDatabase.delete(TABLE_HISTORY, KEY_URL + " = ?", new String[]{url}); - } - - public synchronized void visitHistoryItem(@NonNull String url, @Nullable String title) { - mDatabase = openIfNecessary(); - ContentValues values = new ContentValues(); - values.put(KEY_TITLE, title == null ? "" : title); - values.put(KEY_TIME_VISITED, System.currentTimeMillis()); - Cursor q = mDatabase.query(false, TABLE_HISTORY, new String[]{KEY_URL}, - KEY_URL + " = ?", new String[]{url}, null, null, null, "1"); - if (q.getCount() > 0) { - mDatabase.update(TABLE_HISTORY, values, KEY_URL + " = ?", new String[]{url}); - } else { - addHistoryItem(new HistoryItem(url, title == null ? "" : title)); - } - q.close(); - } - - private synchronized void addHistoryItem(@NonNull HistoryItem item) { - mDatabase = openIfNecessary(); - ContentValues values = new ContentValues(); - values.put(KEY_URL, item.getUrl()); - values.put(KEY_TITLE, item.getTitle()); - values.put(KEY_TIME_VISITED, System.currentTimeMillis()); - mDatabase.insert(TABLE_HISTORY, null, values); - } - - @Nullable - synchronized String getHistoryItem(@NonNull String url) { - mDatabase = openIfNecessary(); - Cursor cursor = mDatabase.query(TABLE_HISTORY, new String[]{KEY_ID, KEY_URL, KEY_TITLE}, - KEY_URL + " = ?", new String[]{url}, null, null, null, null); - String m = null; - if (cursor != null) { - cursor.moveToFirst(); - m = cursor.getString(0); - - cursor.close(); - } - return m; - } - - @NonNull - public synchronized List findItemsContaining(@Nullable String search) { - mDatabase = openIfNecessary(); - List itemList = new ArrayList<>(5); - if (search == null) { - return itemList; - } - search = DatabaseUtils.sqlEscapeString('%' + search + '%'); - - String selectQuery = "SELECT * FROM " + TABLE_HISTORY + " WHERE " + KEY_TITLE + " LIKE " - + search + " OR " + KEY_URL + " LIKE " + search + " ORDER BY " - + KEY_TIME_VISITED + " DESC LIMIT 5"; - Cursor cursor = mDatabase.rawQuery(selectQuery, null); - - int n = 0; - if (cursor.moveToFirst()) { - do { - HistoryItem item = new HistoryItem(); - item.setUrl(cursor.getString(1)); - item.setTitle(cursor.getString(2)); - item.setImageId(R.drawable.ic_history); - itemList.add(item); - n++; - } while (cursor.moveToNext() && n < 5); - } - cursor.close(); - return itemList; - } - - @NonNull - public synchronized List getLastHundredItems() { - mDatabase = openIfNecessary(); - List itemList = new ArrayList<>(100); - String selectQuery = "SELECT * FROM " + TABLE_HISTORY + " ORDER BY " + KEY_TIME_VISITED - + " DESC"; - - Cursor cursor = mDatabase.rawQuery(selectQuery, null); - int counter = 0; - if (cursor.moveToFirst()) { - do { - HistoryItem item = new HistoryItem(); - item.setUrl(cursor.getString(1)); - item.setTitle(cursor.getString(2)); - item.setImageId(R.drawable.ic_history); - itemList.add(item); - counter++; - } while (cursor.moveToNext() && counter < 100); - } - cursor.close(); - return itemList; - } - - @NonNull - public synchronized List getAllHistoryItems() { - mDatabase = openIfNecessary(); - List itemList = new ArrayList<>(); - String selectQuery = "SELECT * FROM " + TABLE_HISTORY + " ORDER BY " + KEY_TIME_VISITED - + " DESC"; - - Cursor cursor = mDatabase.rawQuery(selectQuery, null); - - if (cursor.moveToFirst()) { - do { - HistoryItem item = new HistoryItem(); - item.setUrl(cursor.getString(1)); - item.setTitle(cursor.getString(2)); - item.setImageId(R.drawable.ic_history); - itemList.add(item); - } while (cursor.moveToNext()); - } - cursor.close(); - return itemList; - } - - public synchronized int getHistoryItemsCount() { - mDatabase = openIfNecessary(); - String countQuery = "SELECT * FROM " + TABLE_HISTORY; - Cursor cursor = mDatabase.rawQuery(countQuery, null); - int n = cursor.getCount(); - cursor.close(); - return n; - } -} diff --git a/app/src/main/java/acr/browser/lightning/database/HistoryItem.java b/app/src/main/java/acr/browser/lightning/database/HistoryItem.java index c0c662689..9021e7380 100644 --- a/app/src/main/java/acr/browser/lightning/database/HistoryItem.java +++ b/app/src/main/java/acr/browser/lightning/database/HistoryItem.java @@ -25,19 +25,11 @@ public class HistoryItem implements Comparable { private Bitmap mBitmap = null; private int mImageId = 0; - private int mOrder = 0; + private int mPosition = 0; private boolean mIsFolder = false; public HistoryItem() {} - public HistoryItem(@NonNull HistoryItem item) { - this.mUrl = item.mUrl; - this.mTitle = item.mTitle; - this.mFolder = item.mFolder; - this.mOrder = item.mOrder; - this.mIsFolder = item.mIsFolder; - } - public HistoryItem(@NonNull String url, @NonNull String title) { Preconditions.checkNonNull(url); Preconditions.checkNonNull(title); @@ -63,7 +55,7 @@ public void setImageId(int id) { this.mImageId = id; } - public void setBitmap(Bitmap image) { + public void setBitmap(@Nullable Bitmap image) { mBitmap = image; } @@ -71,12 +63,12 @@ public void setFolder(@Nullable String folder) { mFolder = (folder == null) ? "" : folder; } - public void setOrder(int order) { - mOrder = order; + public void setPosition(int order) { + mPosition = order; } - public int getOrder() { - return mOrder; + public int getPosition() { + return mPosition; } @NonNull @@ -140,8 +132,8 @@ public boolean equals(@Nullable Object object) { HistoryItem that = (HistoryItem) object; return mImageId == that.mImageId && - this.mTitle.equals(that.mTitle) && this.mUrl.equals(that.mUrl) && - this.mFolder.equals(that.mFolder); + this.mTitle.equals(that.mTitle) && this.mUrl.equals(that.mUrl) && + this.mFolder.equals(that.mFolder); } @Override diff --git a/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkDatabase.java b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkDatabase.java new file mode 100644 index 000000000..bb3b9290a --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkDatabase.java @@ -0,0 +1,395 @@ +package acr.browser.lightning.database.bookmark; + +import android.app.Application; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; +import android.text.TextUtils; + +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableAction; +import com.anthonycr.bonsai.CompletableSubscriber; +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleSubscriber; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import acr.browser.lightning.R; +import acr.browser.lightning.constant.Constants; +import acr.browser.lightning.database.HistoryItem; + +/** + * The disk backed bookmark database. + * See {@link BookmarkModel} for method + * documentation. + *

+ * Created by anthonycr on 5/6/17. + */ +@Singleton +public class BookmarkDatabase extends SQLiteOpenHelper implements BookmarkModel { + + private static final String TAG = "BookmarkDatabase"; + + // Database Version + private static final int DATABASE_VERSION = 1; + + // Database Name + private static final String DATABASE_NAME = "bookmarkManager"; + + // HistoryItems table name + private static final String TABLE_BOOKMARK = "bookmark"; + + // HistoryItems Table Columns names + private static final String KEY_ID = "id"; + private static final String KEY_URL = "url"; + private static final String KEY_TITLE = "title"; + private static final String KEY_FOLDER = "folder"; + private static final String KEY_POSITION = "position"; + + @NonNull private final String DEFAULT_BOOKMARK_TITLE; + + @Nullable private SQLiteDatabase mDatabase; + + @Inject + public BookmarkDatabase(@NonNull Application application) { + super(application, DATABASE_NAME, null, DATABASE_VERSION); + DEFAULT_BOOKMARK_TITLE = application.getString(R.string.untitled); + } + + /** + * Lazily initializes the database + * field when called. + * + * @return a non null writable database. + */ + @WorkerThread + @NonNull + private SQLiteDatabase lazyDatabase() { + if (mDatabase == null || !mDatabase.isOpen()) { + mDatabase = getWritableDatabase(); + } + + return mDatabase; + } + + // Creating Tables + @Override + public void onCreate(@NonNull SQLiteDatabase db) { + String CREATE_BOOKMARK_TABLE = "CREATE TABLE " + + DatabaseUtils.sqlEscapeString(TABLE_BOOKMARK) + '(' + + DatabaseUtils.sqlEscapeString(KEY_ID) + " INTEGER PRIMARY KEY," + + DatabaseUtils.sqlEscapeString(KEY_URL) + " TEXT," + + DatabaseUtils.sqlEscapeString(KEY_TITLE) + " TEXT," + + DatabaseUtils.sqlEscapeString(KEY_FOLDER) + " TEXT," + + DatabaseUtils.sqlEscapeString(KEY_POSITION) + " INTEGER" + ')'; + db.execSQL(CREATE_BOOKMARK_TABLE); + } + + // Upgrading database + @Override + public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { + // Drop older table if it exists + db.execSQL("DROP TABLE IF EXISTS " + DatabaseUtils.sqlEscapeString(TABLE_BOOKMARK)); + // Create tables again + onCreate(db); + } + + @NonNull + private static ContentValues bindBookmarkToContentValues(@NonNull HistoryItem bookmarkItem) { + ContentValues contentValues = new ContentValues(4); + contentValues.put(KEY_TITLE, bookmarkItem.getTitle()); + contentValues.put(KEY_URL, bookmarkItem.getUrl()); + contentValues.put(KEY_FOLDER, bookmarkItem.getFolder()); + contentValues.put(KEY_POSITION, bookmarkItem.getPosition()); + + return contentValues; + } + + @NonNull + private static HistoryItem bindCursorToHistoryItem(@NonNull Cursor cursor) { + HistoryItem bookmark = new HistoryItem(); + + bookmark.setImageId(R.drawable.ic_bookmark); + bookmark.setUrl(cursor.getString(cursor.getColumnIndex(KEY_URL))); + bookmark.setTitle(cursor.getString(cursor.getColumnIndex(KEY_TITLE))); + bookmark.setFolder(cursor.getString(cursor.getColumnIndex(KEY_FOLDER))); + bookmark.setPosition(cursor.getInt(cursor.getColumnIndex(KEY_POSITION))); + + return bookmark; + } + + @NonNull + private static List bindCursorToHistoryItemList(@NonNull Cursor cursor) { + List bookmarks = new ArrayList<>(); + + while (cursor.moveToNext()) { + bookmarks.add(bindCursorToHistoryItem(cursor)); + } + + cursor.close(); + + return bookmarks; + } + + @NonNull + @Override + public Single findBookmarkForUrl(@NonNull final String url) { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + Cursor cursor = lazyDatabase().query(TABLE_BOOKMARK, null, KEY_URL + "=?", new String[]{url}, null, null, null, "1"); + + if (cursor.moveToFirst()) { + subscriber.onItem(bindCursorToHistoryItem(cursor)); + } else { + subscriber.onItem(null); + } + + cursor.close(); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single isBookmark(@NonNull final String url) { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + Cursor cursor = lazyDatabase().query(TABLE_BOOKMARK, null, KEY_URL + "=?", new String[]{url}, null, null, null, "1"); + + subscriber.onItem(cursor.moveToFirst()); + + cursor.close(); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single addBookmarkIfNotExists(@NonNull final HistoryItem item) { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + Cursor cursor = lazyDatabase().query(TABLE_BOOKMARK, null, KEY_URL + "=?", new String[]{item.getUrl()}, null, null, null, "1"); + + if (cursor.moveToFirst()) { + cursor.close(); + subscriber.onItem(false); + subscriber.onComplete(); + return; + } + + cursor.close(); + + long id = lazyDatabase().insert(TABLE_BOOKMARK, null, bindBookmarkToContentValues(item)); + + subscriber.onItem(id != -1); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Completable addBookmarkList(@NonNull final List bookmarkItems) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + lazyDatabase().beginTransaction(); + + for (HistoryItem item : bookmarkItems) { + addBookmarkIfNotExists(item).subscribe(); + } + + lazyDatabase().setTransactionSuccessful(); + lazyDatabase().endTransaction(); + + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single deleteBookmark(@NonNull final HistoryItem bookmark) { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + int rows = lazyDatabase().delete(TABLE_BOOKMARK, KEY_URL + "=?", new String[]{bookmark.getUrl()}); + + subscriber.onItem(rows > 0); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Completable renameFolder(@NonNull final String oldName, @NonNull final String newName) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + ContentValues contentValues = new ContentValues(1); + contentValues.put(KEY_FOLDER, newName); + + lazyDatabase().update(TABLE_BOOKMARK, contentValues, KEY_FOLDER + "=?", new String[]{oldName}); + + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Completable deleteFolder(@NonNull final String folderToDelete) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + renameFolder(folderToDelete, "").subscribe(); + + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Completable deleteAllBookmarks() { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + lazyDatabase().delete(TABLE_BOOKMARK, null, null); + + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Completable editBookmark(@NonNull final HistoryItem oldBookmark, @NonNull final HistoryItem newBookmark) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + if (newBookmark.getTitle().isEmpty()) { + newBookmark.setTitle(DEFAULT_BOOKMARK_TITLE); + } + ContentValues contentValues = bindBookmarkToContentValues(newBookmark); + + lazyDatabase().update(TABLE_BOOKMARK, contentValues, KEY_URL + "=?", new String[]{oldBookmark.getUrl()}); + + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single> getAllBookmarks() { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { + Cursor cursor = lazyDatabase().query(TABLE_BOOKMARK, null, null, null, null, null, null); + + subscriber.onItem(bindCursorToHistoryItemList(cursor)); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single> getBookmarksFromFolderSorted(@Nullable final String folder) { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { + String finalFolder = folder != null ? folder : ""; + Cursor cursor = lazyDatabase().query(TABLE_BOOKMARK, null, KEY_FOLDER + "=?", new String[]{finalFolder}, null, null, null); + + List list = bindCursorToHistoryItemList(cursor); + Collections.sort(list); + subscriber.onItem(list); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single> getFoldersSorted() { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { + Cursor cursor = lazyDatabase().query(true, TABLE_BOOKMARK, new String[]{KEY_FOLDER}, null, null, null, null, null, null); + + List folders = new ArrayList<>(); + while (cursor.moveToNext()) { + String folderName = cursor.getString(cursor.getColumnIndex(KEY_FOLDER)); + if (TextUtils.isEmpty(folderName)) { + continue; + } + + final HistoryItem folder = new HistoryItem(); + folder.setIsFolder(true); + folder.setTitle(folderName); + folder.setImageId(R.drawable.ic_folder); + folder.setUrl(Constants.FOLDER + folderName); + + folders.add(folder); + } + + cursor.close(); + + Collections.sort(folders); + subscriber.onItem(folders); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single> getFolderNames() { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { + Cursor cursor = lazyDatabase().query(true, TABLE_BOOKMARK, new String[]{KEY_FOLDER}, null, null, null, null, null, null); + + List folders = new ArrayList<>(); + while (cursor.moveToNext()) { + String folderName = cursor.getString(cursor.getColumnIndex(KEY_FOLDER)); + if (TextUtils.isEmpty(folderName)) { + continue; + } + + folders.add(folderName); + } + + cursor.close(); + + subscriber.onItem(folders); + subscriber.onComplete(); + } + }); + } + + @Override + public long count() { + return DatabaseUtils.queryNumEntries(lazyDatabase(), TABLE_BOOKMARK); + } + +} diff --git a/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkExporter.java b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkExporter.java new file mode 100644 index 000000000..005f395b3 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkExporter.java @@ -0,0 +1,201 @@ +package acr.browser.lightning.database.bookmark; + +import android.content.Context; +import android.os.Environment; +import android.support.annotation.NonNull; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableAction; +import com.anthonycr.bonsai.CompletableSubscriber; +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleSubscriber; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import acr.browser.lightning.R; +import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.utils.Preconditions; +import acr.browser.lightning.utils.Utils; + +/** + * The class responsible for importing and exporting + * bookmarks in the JSON format. + *

+ * Created by anthonycr on 5/7/17. + */ +public class BookmarkExporter { + + private static final String TAG = "BookmarkExporter"; + + private static final String KEY_URL = "url"; + private static final String KEY_TITLE = "title"; + private static final String KEY_FOLDER = "folder"; + private static final String KEY_ORDER = "order"; + + /** + * Retrieves all the default bookmarks stored + * in the raw file within assets. + * + * @param context the context necessary to open assets. + * @return a non null list of the bookmarks stored in assets. + */ + @NonNull + public static List importBookmarksFromAssets(@NonNull Context context) { + List bookmarks = new ArrayList<>(); + BufferedReader bookmarksReader = null; + InputStream inputStream = null; + try { + inputStream = context.getResources().openRawResource(R.raw.default_bookmarks); + //noinspection IOResourceOpenedButNotSafelyClosed + bookmarksReader = new BufferedReader(new InputStreamReader(inputStream)); + String line; + while ((line = bookmarksReader.readLine()) != null) { + try { + JSONObject object = new JSONObject(line); + HistoryItem item = new HistoryItem(); + item.setTitle(object.getString(KEY_TITLE)); + final String url = object.getString(KEY_URL); + item.setUrl(url); + item.setFolder(object.getString(KEY_FOLDER)); + item.setPosition(object.getInt(KEY_ORDER)); + item.setImageId(R.drawable.ic_bookmark); + bookmarks.add(item); + } catch (JSONException e) { + Log.e(TAG, "Can't parse line " + line, e); + } + } + } catch (IOException e) { + Log.e(TAG, "Error reading the bookmarks file", e); + } finally { + Utils.close(bookmarksReader); + Utils.close(inputStream); + } + + return bookmarks; + } + + /** + * Exports the list of bookmarks to a file. + * + * @param bookmarkList the bookmarks to export. + * @param file the file to export to. + * @return an observable that emits a completion + * event when the export is complete, or an error + * event if there is a problem. + */ + @NonNull + public static Completable exportBookmarksToFile(@NonNull final List bookmarkList, + @NonNull final File file) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull final CompletableSubscriber subscriber) { + Preconditions.checkNonNull(bookmarkList); + BufferedWriter bookmarkWriter = null; + try { + //noinspection IOResourceOpenedButNotSafelyClosed + bookmarkWriter = new BufferedWriter(new FileWriter(file, false)); + + JSONObject object = new JSONObject(); + for (HistoryItem item : bookmarkList) { + object.put(KEY_TITLE, item.getTitle()); + object.put(KEY_URL, item.getUrl()); + object.put(KEY_FOLDER, item.getFolder()); + object.put(KEY_ORDER, item.getPosition()); + bookmarkWriter.write(object.toString()); + bookmarkWriter.newLine(); + } + subscriber.onComplete(); + } catch (@NonNull IOException | JSONException e) { + subscriber.onError(e); + } finally { + Utils.close(bookmarkWriter); + } + } + }); + } + + /** + * Attempts to import bookmarks from the + * given file. If the file is not in a + * supported format, it will fail. + * + * @param file the file to import from. + * @return an observable that emits the + * imported bookmarks, or an error if the + * file cannot be imported. + */ + @NonNull + public static Single> importBookmarksFromFile(@NonNull final File file) { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { + BufferedReader bookmarksReader = null; + try { + //noinspection IOResourceOpenedButNotSafelyClosed + bookmarksReader = new BufferedReader(new FileReader(file)); + String line; + + List bookmarks = new ArrayList<>(); + while ((line = bookmarksReader.readLine()) != null) { + JSONObject object = new JSONObject(line); + HistoryItem item = new HistoryItem(); + item.setTitle(object.getString(KEY_TITLE)); + item.setUrl(object.getString(KEY_URL)); + item.setFolder(object.getString(KEY_FOLDER)); + item.setPosition(object.getInt(KEY_ORDER)); + bookmarks.add(item); + } + + subscriber.onItem(bookmarks); + subscriber.onComplete(); + } catch (IOException | JSONException e) { + subscriber.onError(e); + } finally { + Utils.close(bookmarksReader); + } + } + }); + } + + /** + * A blocking call that creates a new export file with + * the name "BookmarkExport.txt" and an appropriate + * numerical appendage if a file already exists with + * that name. + * + * @return a non null empty file that can be used + * to export bookmarks to. + */ + @WorkerThread + @NonNull + public static File createNewExportFile() { + File bookmarksExport = new File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "BookmarksExport.txt"); + int counter = 0; + while (bookmarksExport.exists()) { + counter++; + bookmarksExport = new File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "BookmarksExport-" + counter + ".txt"); + } + + return bookmarksExport; + } + +} diff --git a/app/src/main/java/acr/browser/lightning/database/BookmarkLocalSync.java b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkLocalSync.java similarity index 80% rename from app/src/main/java/acr/browser/lightning/database/BookmarkLocalSync.java rename to app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkLocalSync.java index bf15463a1..d7bd6d7d6 100644 --- a/app/src/main/java/acr/browser/lightning/database/BookmarkLocalSync.java +++ b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkLocalSync.java @@ -1,4 +1,4 @@ -package acr.browser.lightning.database; +package acr.browser.lightning.database.bookmark; import android.content.Context; import android.database.Cursor; @@ -8,17 +8,19 @@ import android.support.annotation.WorkerThread; import android.util.Log; +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleSubscriber; + import java.util.ArrayList; import java.util.List; -import com.anthonycr.bonsai.Action; -import com.anthonycr.bonsai.Observable; -import com.anthonycr.bonsai.Subscriber; +import acr.browser.lightning.database.HistoryItem; import acr.browser.lightning.utils.Utils; public class BookmarkLocalSync { - private static final String TAG = BookmarkLocalSync.class.getSimpleName(); + private static final String TAG = "BookmarkLocalSync"; private static final String STOCK_BOOKMARKS_CONTENT = "content://browser/bookmarks"; private static final String CHROME_BOOKMARKS_CONTENT = "content://com.android.chrome.browser/bookmarks"; @@ -42,6 +44,7 @@ public BookmarkLocalSync(@NonNull Context context) { mContext = context; } + @NonNull private List getBookmarksFromContentUri(String contentUri) { List list = new ArrayList<>(); Cursor cursor = getBrowserCursor(contentUri); @@ -81,7 +84,7 @@ private Cursor getBrowserCursor(String contentUri) { Uri uri = Uri.parse(contentUri); try { cursor = mContext.getContentResolver().query(uri, - new String[]{COLUMN_URL, COLUMN_TITLE, COLUMN_BOOKMARK}, null, null, null); + new String[]{COLUMN_URL, COLUMN_TITLE, COLUMN_BOOKMARK}, null, null, null); } catch (IllegalArgumentException e) { return null; } @@ -89,10 +92,10 @@ private Cursor getBrowserCursor(String contentUri) { } @NonNull - public Observable> getSupportedBrowsers() { - return Observable.create(new Action>() { + public Single> getSupportedBrowsers() { + return Single.create(new SingleAction>() { @Override - public void onSubscribe(@NonNull Subscriber> subscriber) { + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { List sources = new ArrayList<>(1); if (isBrowserSupported(STOCK_BOOKMARKS_CONTENT)) { sources.add(Source.STOCK); @@ -106,7 +109,7 @@ public void onSubscribe(@NonNull Subscriber> subscriber) { if (isBrowserSupported(CHROME_DEV_BOOKMARKS_CONTENT)) { sources.add(Source.CHROME_DEV); } - subscriber.onNext(sources); + subscriber.onItem(sources); subscriber.onComplete(); } }); @@ -143,16 +146,23 @@ public List getBookmarksFromChromeDev() { return getBookmarksFromContentUri(CHROME_DEV_BOOKMARKS_CONTENT); } - @WorkerThread - public boolean isBrowserImportSupported() { - Cursor chrome = getChromeCursor(); - Utils.close(chrome); - Cursor dev = getChromeDevCursor(); - Utils.close(dev); - Cursor beta = getChromeBetaCursor(); - Cursor stock = getStockCursor(); - Utils.close(stock); - return chrome != null || dev != null || beta != null || stock != null; + @NonNull + public Single isBrowserImportSupported() { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + Cursor chrome = getChromeCursor(); + Utils.close(chrome); + Cursor dev = getChromeDevCursor(); + Utils.close(dev); + Cursor beta = getChromeBetaCursor(); + Cursor stock = getStockCursor(); + Utils.close(stock); + + subscriber.onItem(chrome != null || dev != null || beta != null || stock != null); + subscriber.onComplete(); + } + }); } @Nullable diff --git a/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkModel.java b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkModel.java new file mode 100644 index 000000000..ec10e06d9 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/database/bookmark/BookmarkModel.java @@ -0,0 +1,164 @@ +package acr.browser.lightning.database.bookmark; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; + +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.Single; + +import java.util.List; + +import acr.browser.lightning.database.HistoryItem; + +/** + * The interface that should be used to + * communicate with the bookmark database. + *

+ * Created by anthonycr on 5/6/17. + */ +public interface BookmarkModel { + + /** + * Gets the bookmark associated with the URL. + * + * @param url the URL to look for. + * @return an observable that will emit either + * the bookmark associated with the URL or null. + */ + @NonNull + Single findBookmarkForUrl(@NonNull String url); + + /** + * Determines if a URL is associated with a bookmark. + * + * @param url the URL to check. + * @return an observable that will emit true if + * the URL is a bookmark, false otherwise. + */ + @NonNull + Single isBookmark(@NonNull String url); + + /** + * Adds a bookmark if one does not already exist with + * the same URL. + * + * @param item the bookmark to add. + * @return an observable that emits true if the bookmark + * was added, false otherwise. + */ + @NonNull + Single addBookmarkIfNotExists(@NonNull HistoryItem item); + + /** + * Adds a list of bookmarks to the database. + * + * @param bookmarkItems the bookmarks to add. + * @return an observable that emits a complete event + * when all the bookmarks have been added. + */ + @NonNull + Completable addBookmarkList(@NonNull List bookmarkItems); + + /** + * Deletes a bookmark from the database. + * + * @param bookmark the bookmark to delete. + * @return an observable that emits true when + * the bookmark is deleted, false otherwise. + */ + @NonNull + Single deleteBookmark(@NonNull HistoryItem bookmark); + + /** + * Moves all bookmarks in the old folder to the new folder. + * + * @param oldName the name of the old folder. + * @param newName the name of the new folder. + * @return an observable that emits a completion + * event when the folder is renamed. + */ + @NonNull + Completable renameFolder(@NonNull String oldName, @NonNull String newName); + + /** + * Deletes a folder from the database, all bookmarks + * in that folder will be moved to the root level. + * + * @param folderToDelete the folder to delete. + * @return an observable that emits a completion + * event when the folder has been deleted. + */ + @NonNull + Completable deleteFolder(@NonNull String folderToDelete); + + /** + * Deletes all bookmarks in the database. + * + * @return an observable that emits a completion + * event when all bookmarks have been deleted. + */ + @NonNull + Completable deleteAllBookmarks(); + + /** + * Changes the bookmark with the original URL + * with all the data from the new bookmark. + * + * @param oldBookmark the old bookmark to replace. + * @param newBookmark the new bookmark. + * @return an observable that emits a completion event + * when the bookmark edit is done. + */ + @NonNull + Completable editBookmark(@NonNull HistoryItem oldBookmark, @NonNull HistoryItem newBookmark); + + /** + * Emits a list of all bookmarks + * + * @return an observable that emits a list + * of all bookmarks. + */ + @NonNull + Single> getAllBookmarks(); + + /** + * Emits all bookmarks in a certain folder. + * If the folder chosen is null, then all bookmarks + * without a specified folder will be returned. + * + * @param folder gets the bookmarks from this folder, may be null. + * @return an observable that emits a list of bookmarks + * in the given folder. + */ + @NonNull + Single> getBookmarksFromFolderSorted(@Nullable String folder); + + /** + * Returns all folders as {@link HistoryItem}. + * The root folder is omitted. + * + * @return an observable that emits a list of folders. + */ + @NonNull + Single> getFoldersSorted(); + + /** + * Returns the names of all folders. + * The root folder is omitted. + * + * @return an observable that emits a list of folder names. + */ + @NonNull + Single> getFolderNames(); + + /** + * A synchronous call to the model + * that returns the number of bookmarks. + * Should be called from a background thread. + * + * @return the number of bookmarks in the database. + */ + @WorkerThread + long count(); +} diff --git a/app/src/main/java/acr/browser/lightning/database/bookmark/legacy/LegacyBookmarkManager.java b/app/src/main/java/acr/browser/lightning/database/bookmark/legacy/LegacyBookmarkManager.java new file mode 100644 index 000000000..50de3e683 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/database/bookmark/legacy/LegacyBookmarkManager.java @@ -0,0 +1,112 @@ +package acr.browser.lightning.database.bookmark.legacy; + +import android.app.Application; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +import acr.browser.lightning.R; +import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.utils.Utils; + +@Deprecated +public class LegacyBookmarkManager { + + private static final String TAG = "LegacyBookmarkManager"; + + private static final String TITLE = "title"; + private static final String URL = "url"; + private static final String FOLDER = "folder"; + private static final String ORDER = "order"; + private static final String FILE_BOOKMARKS = "bookmarks.dat"; + + /** + * Gets all bookmarks from the old bookmark file + * and then deletes the file. + * + * @param application the context needed to open the file. + * @return a list of bookmarks from the old bookmark file. + */ + @WorkerThread + @NonNull + public static List destructiveGetBookmarks(@NonNull Application application) { + File filesDir = application.getFilesDir(); + List bookmarks = new ArrayList<>(); + final File bookmarksFile = new File(filesDir, FILE_BOOKMARKS); + + BufferedReader bookmarksReader = null; + InputStream inputStream = null; + try { + if (bookmarksFile.exists() && bookmarksFile.isFile()) { + //noinspection IOResourceOpenedButNotSafelyClosed + inputStream = new FileInputStream(bookmarksFile); + } else { + return bookmarks; + } + //noinspection IOResourceOpenedButNotSafelyClosed + bookmarksReader = new BufferedReader(new InputStreamReader(inputStream)); + String line; + while ((line = bookmarksReader.readLine()) != null) { + try { + JSONObject object = new JSONObject(line); + + HistoryItem item = new HistoryItem(); + + item.setTitle(object.getString(TITLE)); + item.setUrl(object.getString(URL)); + item.setFolder(object.getString(FOLDER)); + item.setPosition(object.getInt(ORDER)); + item.setImageId(R.drawable.ic_bookmark); + + bookmarks.add(item); + } catch (JSONException e) { + Log.e(TAG, "Can't parse line " + line, e); + } + } + } catch (IOException e) { + Log.e(TAG, "Error reading the bookmarks file", e); + } finally { + Utils.close(bookmarksReader); + Utils.close(inputStream); + } + + bookmarksFile.delete(); + + return bookmarks; + } + + /** + * This class sorts bookmarks alphabetically, with folders coming after bookmarks + */ + private static class SortIgnoreCase implements Comparator { + + public int compare(@Nullable HistoryItem o1, @Nullable HistoryItem o2) { + if (o1 == null || o2 == null) { + return 0; + } + if (o1.isFolder() == o2.isFolder()) { + return o1.getTitle().toLowerCase(Locale.getDefault()) + .compareTo(o2.getTitle().toLowerCase(Locale.getDefault())); + + } else { + return o1.isFolder() ? 1 : -1; + } + } + + } +} diff --git a/app/src/main/java/acr/browser/lightning/database/downloads/DownloadItem.java b/app/src/main/java/acr/browser/lightning/database/downloads/DownloadItem.java new file mode 100644 index 000000000..90025ba37 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/database/downloads/DownloadItem.java @@ -0,0 +1,97 @@ +/* + * Copyright 2014 A.C.R. Development + */ +package acr.browser.lightning.database.downloads; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import acr.browser.lightning.utils.Preconditions; + +public class DownloadItem implements Comparable { + + // private variables + @NonNull + private String mUrl = ""; + + @NonNull + private String mTitle = ""; + + @NonNull + private String mContentSize = ""; + + public DownloadItem() {} + + public DownloadItem(@NonNull String url, @NonNull String title, @NonNull String size) { + Preconditions.checkNonNull(url); + Preconditions.checkNonNull(title); + Preconditions.checkNonNull(size); + this.mUrl = url; + this.mTitle = title; + this.mContentSize = size; + } + + @NonNull + public String getUrl() { + return this.mUrl; + } + + public void setUrl(@Nullable String url) { + this.mUrl = (url == null) ? "" : url; + } + + @NonNull + public String getTitle() { + return this.mTitle; + } + + public void setTitle(@Nullable String title) { + this.mTitle = (title == null) ? "" : title; + } + + @NonNull + public String getContentSize() { + return this.mContentSize; + } + + public void setContentSize(@Nullable String size) { + this.mContentSize = (size == null) ? "" : size; + } + + @NonNull + @Override + public String toString() { + return mTitle; + } + + @Override + public int compareTo(@NonNull DownloadItem another) { + int compare = this.mTitle.compareTo(another.mTitle); + if (compare == 0) { + return this.mUrl.compareTo(another.mUrl); + } + return compare; + } + + @Override + public boolean equals(@Nullable Object object) { + + if (this == object) return true; + if (object == null) return false; + if (!(object instanceof DownloadItem)) return false; + + DownloadItem that = (DownloadItem) object; + + return this.mTitle.equals(that.mTitle) && this.mUrl.equals(that.mUrl) + && this.mContentSize.equals(that.mContentSize); + } + + @Override + public int hashCode() { + + int result = mUrl.hashCode(); + result = 31 * result + mTitle.hashCode(); + + return result; + } +} diff --git a/app/src/main/java/acr/browser/lightning/database/downloads/DownloadsDatabase.java b/app/src/main/java/acr/browser/lightning/database/downloads/DownloadsDatabase.java new file mode 100644 index 000000000..a14faff43 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/database/downloads/DownloadsDatabase.java @@ -0,0 +1,261 @@ +package acr.browser.lightning.database.downloads; + +import android.app.Application; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; + +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableAction; +import com.anthonycr.bonsai.CompletableSubscriber; +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleSubscriber; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import acr.browser.lightning.R; + +/** + * The disk backed download database. + * See {@link DownloadsModel} for method + * documentation. + */ +@Singleton +public class DownloadsDatabase extends SQLiteOpenHelper implements DownloadsModel { + + private static final String TAG = "DownloadsDatabase"; + + // Database Version + private static final int DATABASE_VERSION = 1; + + // Database Name + private static final String DATABASE_NAME = "downloadManager"; + + // HistoryItems table name + private static final String TABLE_DOWNLOADS = "download"; + + // HistoryItems Table Columns names + private static final String KEY_ID = "id"; + private static final String KEY_URL = "url"; + private static final String KEY_TITLE = "title"; + private static final String KEY_SIZE = "size"; + + @NonNull private final String DEFAULT_DOWNLOADS_TITLE; + + @Nullable private SQLiteDatabase mDatabase; + + @Inject + public DownloadsDatabase(@NonNull Application application) { + super(application, DATABASE_NAME, null, DATABASE_VERSION); + DEFAULT_DOWNLOADS_TITLE = application.getString(R.string.untitled); + } + + /** + * Lazily initializes the database + * field when called. + * + * @return a non null writable database. + */ + @WorkerThread + @NonNull + private SQLiteDatabase lazyDatabase() { + if (mDatabase == null || !mDatabase.isOpen()) { + mDatabase = getWritableDatabase(); + } + + return mDatabase; + } + + // Creating Tables + @Override + public void onCreate(@NonNull SQLiteDatabase db) { + String CREATE_BOOKMARK_TABLE = "CREATE TABLE " + + DatabaseUtils.sqlEscapeString(TABLE_DOWNLOADS) + '(' + + DatabaseUtils.sqlEscapeString(KEY_ID) + " INTEGER PRIMARY KEY," + + DatabaseUtils.sqlEscapeString(KEY_URL) + " TEXT," + + DatabaseUtils.sqlEscapeString(KEY_TITLE) + " TEXT," + + DatabaseUtils.sqlEscapeString(KEY_SIZE) + " TEXT" + ')'; + db.execSQL(CREATE_BOOKMARK_TABLE); + } + + // Upgrading database + @Override + public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { + // Drop older table if it exists + db.execSQL("DROP TABLE IF EXISTS " + DatabaseUtils.sqlEscapeString(TABLE_DOWNLOADS)); + // Create tables again + onCreate(db); + } + + @NonNull + private static ContentValues bindBookmarkToContentValues(@NonNull DownloadItem downloadItem) { + ContentValues contentValues = new ContentValues(3); + contentValues.put(KEY_TITLE, downloadItem.getTitle()); + contentValues.put(KEY_URL, downloadItem.getUrl()); + contentValues.put(KEY_SIZE, downloadItem.getContentSize()); + + return contentValues; + } + + @NonNull + private static DownloadItem bindCursorToDownloadItem(@NonNull Cursor cursor) { + DownloadItem download = new DownloadItem(); + + download.setUrl(cursor.getString(cursor.getColumnIndex(KEY_URL))); + download.setTitle(cursor.getString(cursor.getColumnIndex(KEY_TITLE))); + download.setContentSize(cursor.getString(cursor.getColumnIndex(KEY_SIZE))); + + return download; + } + + @NonNull + private static List bindCursorToDownloadItemList(@NonNull Cursor cursor) { + List downloads = new ArrayList<>(); + + while (cursor.moveToNext()) { + downloads.add(bindCursorToDownloadItem(cursor)); + } + + cursor.close(); + + return downloads; + } + + @NonNull + @Override + public Single findDownloadForUrl(@NonNull final String url) { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + Cursor cursor = lazyDatabase().query(TABLE_DOWNLOADS, null, KEY_URL + "=?", new String[]{url}, null, null, "1"); + + if (cursor.moveToFirst()) { + subscriber.onItem(bindCursorToDownloadItem(cursor)); + } else { + subscriber.onItem(null); + } + + cursor.close(); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single isDownload(@NonNull final String url) { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + Cursor cursor = lazyDatabase().query(TABLE_DOWNLOADS, null, KEY_URL + "=?", new String[]{url}, null, null, null, "1"); + + subscriber.onItem(cursor.moveToFirst()); + + cursor.close(); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single addDownloadIfNotExists(@NonNull final DownloadItem item) { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + Cursor cursor = lazyDatabase().query(TABLE_DOWNLOADS, null, KEY_URL + "=?", new String[]{item.getUrl()}, null, null, "1"); + + if (cursor.moveToFirst()) { + cursor.close(); + subscriber.onItem(false); + subscriber.onComplete(); + return; + } + + cursor.close(); + + long id = lazyDatabase().insert(TABLE_DOWNLOADS, null, bindBookmarkToContentValues(item)); + + subscriber.onItem(id != -1); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Completable addDownloadsList(@NonNull final List bookmarkItems) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + lazyDatabase().beginTransaction(); + + for (DownloadItem item : bookmarkItems) { + addDownloadIfNotExists(item).subscribe(); + } + + lazyDatabase().setTransactionSuccessful(); + lazyDatabase().endTransaction(); + + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single deleteDownload(@NonNull final String url) { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + int rows = lazyDatabase().delete(TABLE_DOWNLOADS, KEY_URL + "=?", new String[]{url}); + + subscriber.onItem(rows > 0); + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Completable deleteAllDownloads() { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + lazyDatabase().delete(TABLE_DOWNLOADS, null, null); + + subscriber.onComplete(); + } + }); + } + + @NonNull + @Override + public Single> getAllDownloads() { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { + Cursor cursor = lazyDatabase().query(TABLE_DOWNLOADS, null, null, null, null, null, null); + + subscriber.onItem(bindCursorToDownloadItemList(cursor)); + subscriber.onComplete(); + } + }); + } + + @Override + public long count() { + return DatabaseUtils.queryNumEntries(lazyDatabase(), TABLE_DOWNLOADS); + } + +} diff --git a/app/src/main/java/acr/browser/lightning/database/downloads/DownloadsModel.java b/app/src/main/java/acr/browser/lightning/database/downloads/DownloadsModel.java new file mode 100644 index 000000000..a5545ef3e --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/database/downloads/DownloadsModel.java @@ -0,0 +1,97 @@ +package acr.browser.lightning.database.downloads; + +import android.support.annotation.NonNull; +import android.support.annotation.WorkerThread; + +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.Single; + +import java.util.List; + +/** + * The interface that should be used to + * communicate with the download database. + *

+ * Created by df1e on 29/5/17. + */ +public interface DownloadsModel { + + /** + * Determines if a URL is associated with a download. + * + * @param url the URL to check. + * @return an observable that will emit true if + * the URL is a download, false otherwise. + */ + @NonNull + Single isDownload(@NonNull String url); + + /** + * Gets the download associated with the URL. + * + * @param url the URL to look for. + * @return an observable that will emit either + * the download associated with the URL or null. + */ + @NonNull + Single findDownloadForUrl(@NonNull String url); + + /** + * Adds a download if one does not already exist with + * the same URL. + * + * @param item the download to add. + * @return an observable that emits true if the download + * was added, false otherwise. + */ + @NonNull + Single addDownloadIfNotExists(@NonNull DownloadItem item); + + /** + * Adds a list of downloads to the database. + * + * @param downloadItems the downloads to add. + * @return an observable that emits a complete event + * when all the downloads have been added. + */ + @NonNull + Completable addDownloadsList(@NonNull List downloadItems); + + /** + * Deletes a download from the database. + * + * @param url the download url to delete. + * @return an observable that emits true when + * the download is deleted, false otherwise. + */ + @NonNull + Single deleteDownload(@NonNull String url); + + /** + * Deletes all downloads in the database. + * + * @return an observable that emits a completion + * event when all downloads have been deleted. + */ + @NonNull + Completable deleteAllDownloads(); + + /** + * Emits a list of all downloads + * + * @return an observable that emits a list + * of all downloads. + */ + @NonNull + Single> getAllDownloads(); + + /** + * A synchronous call to the model + * that returns the number of downloads. + * Should be called from a background thread. + * + * @return the number of downloads in the database. + */ + @WorkerThread + long count(); +} diff --git a/app/src/main/java/acr/browser/lightning/database/history/HistoryDatabase.java b/app/src/main/java/acr/browser/lightning/database/history/HistoryDatabase.java new file mode 100644 index 000000000..e4e04ec3d --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/database/history/HistoryDatabase.java @@ -0,0 +1,200 @@ +/* + * Copyright 2014 A.C.R. Development + */ +package acr.browser.lightning.database.history; + +import android.app.Application; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import acr.browser.lightning.R; +import acr.browser.lightning.database.HistoryItem; + +@Singleton +@WorkerThread +public class HistoryDatabase extends SQLiteOpenHelper { + + // All Static variables + // Database Version + private static final int DATABASE_VERSION = 2; + + // Database Name + private static final String DATABASE_NAME = "historyManager"; + + // HistoryItems table name + private static final String TABLE_HISTORY = "history"; + + // HistoryItems Table Columns names + private static final String KEY_ID = "id"; + private static final String KEY_URL = "url"; + private static final String KEY_TITLE = "title"; + private static final String KEY_TIME_VISITED = "time"; + + @Nullable private SQLiteDatabase mDatabase; + + @Inject + HistoryDatabase(@NonNull Application application) { + super(application, DATABASE_NAME, null, DATABASE_VERSION); + mDatabase = HistoryDatabase.this.getWritableDatabase(); + } + + // Creating Tables + @Override + public void onCreate(@NonNull SQLiteDatabase db) { + String CREATE_HISTORY_TABLE = "CREATE TABLE " + TABLE_HISTORY + '(' + KEY_ID + + " INTEGER PRIMARY KEY," + KEY_URL + " TEXT," + KEY_TITLE + " TEXT," + + KEY_TIME_VISITED + " INTEGER" + ')'; + db.execSQL(CREATE_HISTORY_TABLE); + } + + // Upgrading database + @Override + public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) { + // Drop older table if it exists + db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY); + // Create tables again + onCreate(db); + } + + @NonNull + private static HistoryItem fromCursor(@NonNull Cursor cursor) { + HistoryItem historyItem = new HistoryItem(); + historyItem.setUrl(cursor.getString(1)); + historyItem.setTitle(cursor.getString(2)); + historyItem.setImageId(R.drawable.ic_history); + + return historyItem; + } + + @WorkerThread + @NonNull + private SQLiteDatabase lazyDatabase() { + if (mDatabase == null || !mDatabase.isOpen()) { + mDatabase = this.getWritableDatabase(); + } + return mDatabase; + } + + @WorkerThread + synchronized void deleteHistory() { + lazyDatabase().delete(TABLE_HISTORY, null, null); + lazyDatabase().close(); + } + + @WorkerThread + synchronized void deleteHistoryItem(@NonNull String url) { + lazyDatabase().delete(TABLE_HISTORY, KEY_URL + " = ?", new String[]{url}); + } + + @WorkerThread + synchronized void visitHistoryItem(@NonNull String url, @Nullable String title) { + ContentValues values = new ContentValues(); + values.put(KEY_TITLE, title == null ? "" : title); + values.put(KEY_TIME_VISITED, System.currentTimeMillis()); + + Cursor cursor = lazyDatabase().query(false, TABLE_HISTORY, new String[]{KEY_URL}, + KEY_URL + " = ?", new String[]{url}, null, null, null, "1"); + + if (cursor.getCount() > 0) { + lazyDatabase().update(TABLE_HISTORY, values, KEY_URL + " = ?", new String[]{url}); + } else { + addHistoryItem(new HistoryItem(url, title == null ? "" : title)); + } + + cursor.close(); + } + + @WorkerThread + private synchronized void addHistoryItem(@NonNull HistoryItem item) { + ContentValues values = new ContentValues(); + values.put(KEY_URL, item.getUrl()); + values.put(KEY_TITLE, item.getTitle()); + values.put(KEY_TIME_VISITED, System.currentTimeMillis()); + lazyDatabase().insert(TABLE_HISTORY, null, values); + } + + @WorkerThread + @Nullable + synchronized String getHistoryItem(@NonNull String url) { + Cursor cursor = lazyDatabase().query(TABLE_HISTORY, new String[]{KEY_ID, KEY_URL, KEY_TITLE}, + KEY_URL + " = ?", new String[]{url}, null, null, null, "1"); + String m = null; + if (cursor != null) { + cursor.moveToFirst(); + m = cursor.getString(0); + + cursor.close(); + } + return m; + } + + @WorkerThread + @NonNull + synchronized List findItemsContaining(@Nullable String search) { + List itemList = new ArrayList<>(5); + if (search == null) { + return itemList; + } + + search = '%' + search + '%'; + + Cursor cursor = lazyDatabase().query(TABLE_HISTORY, null, KEY_TITLE + " LIKE ? OR " + KEY_URL + " LIKE ?", + new String[]{search, search}, null, null, KEY_TIME_VISITED + " DESC", "5"); + + while (cursor.moveToNext()) { + itemList.add(fromCursor(cursor)); + } + + cursor.close(); + + return itemList; + } + + @WorkerThread + @NonNull + synchronized List getLastHundredItems() { + List itemList = new ArrayList<>(100); + Cursor cursor = lazyDatabase().query(TABLE_HISTORY, null, null, null, null, null, KEY_TIME_VISITED + " DESC", "100"); + + while (cursor.moveToNext()) { + itemList.add(fromCursor(cursor)); + } + + cursor.close(); + + return itemList; + } + + @WorkerThread + @NonNull + synchronized List getAllHistoryItems() { + List itemList = new ArrayList<>(); + + Cursor cursor = lazyDatabase().query(TABLE_HISTORY, null, null, null, null, null, KEY_TIME_VISITED + " DESC"); + + while (cursor.moveToNext()) { + itemList.add(fromCursor(cursor)); + } + + cursor.close(); + + return itemList; + } + + @WorkerThread + synchronized long getHistoryItemsCount() { + return DatabaseUtils.queryNumEntries(mDatabase, TABLE_HISTORY); + } +} diff --git a/app/src/main/java/acr/browser/lightning/database/history/HistoryModel.java b/app/src/main/java/acr/browser/lightning/database/history/HistoryModel.java new file mode 100644 index 000000000..185ebcf92 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/database/history/HistoryModel.java @@ -0,0 +1,136 @@ +package acr.browser.lightning.database.history; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableAction; +import com.anthonycr.bonsai.CompletableSubscriber; +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleSubscriber; + +import java.util.List; + +import acr.browser.lightning.app.BrowserApp; +import acr.browser.lightning.database.HistoryItem; + +/** + * A model class providing reactive bindings + * with the underlying history database. + */ +public final class HistoryModel { + + private HistoryModel() {} + + /** + * An observable that deletes browser history. + * + * @return a valid observable. + */ + @NonNull + public static Completable deleteHistory() { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + BrowserApp.getAppComponent() + .historyDatabase() + .deleteHistory(); + + subscriber.onComplete(); + } + }); + } + + /** + * An observable that deletes the history + * entry with the specific URL. + * + * @param url the URL of the item to delete. + * @return a valid observable. + */ + @NonNull + public static Completable deleteHistoryItem(@NonNull final String url) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + BrowserApp.getAppComponent() + .historyDatabase() + .deleteHistoryItem(url); + + subscriber.onComplete(); + } + }); + } + + /** + * An observable that visits the URL by + * adding it to the database if it doesn't + * exist or updating the time visited if + * it does. + * + * @param url the URL of the item that was visited. + * @param title the title of the item that was visited. + * @return a valid observable. + */ + @NonNull + public static Completable visitHistoryItem(@NonNull final String url, @Nullable final String title) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + BrowserApp.getAppComponent() + .historyDatabase() + .visitHistoryItem(url, title); + + subscriber.onComplete(); + } + }); + } + + /** + * An observable that finds all history items + * containing the given query. If the query + * is contained anywhere within the title or + * the URL of the history item, it will be + * returned. For the sake of performance, only + * the first five items will be emitted. + * + * @param query the query to search for. + * @return a valid observable that emits + * a list of history items. + */ + @NonNull + public static Single> findHistoryItemsContaining(@NonNull final String query) { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { + List result = BrowserApp.getAppComponent() + .historyDatabase().findItemsContaining(query); + + subscriber.onItem(result); + subscriber.onComplete(); + } + }); + } + + /** + * An observable that emits a list of the + * last 100 visited history items. + * + * @return a valid observable that emits + * a list of history items. + */ + @NonNull + public static Single> lastHundredVisitedHistoryItems() { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { + List result = BrowserApp.getAppComponent() + .historyDatabase().getLastHundredItems(); + + subscriber.onItem(result); + subscriber.onComplete(); + } + }); + } +} diff --git a/app/src/main/java/acr/browser/lightning/dialog/BrowserDialog.java b/app/src/main/java/acr/browser/lightning/dialog/BrowserDialog.java index 15a240ce6..e6e8f2826 100644 --- a/app/src/main/java/acr/browser/lightning/dialog/BrowserDialog.java +++ b/app/src/main/java/acr/browser/lightning/dialog/BrowserDialog.java @@ -12,10 +12,10 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.Window; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; -import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; @@ -25,7 +25,6 @@ import acr.browser.lightning.R; import acr.browser.lightning.utils.DeviceUtils; import acr.browser.lightning.utils.ResourceUtils; -import acr.browser.lightning.utils.Utils; /** * Copyright 7/31/2016 Anthony Restaino @@ -44,10 +43,6 @@ */ public class BrowserDialog { - public interface Listener { - void onClick(); - } - public interface EditorListener { void onClick(String text); } @@ -57,12 +52,12 @@ public static abstract class Item { private final int mTitle; private boolean mCondition = true; - public Item(@StringRes int title, boolean condition) { + Item(@StringRes int title, boolean condition) { this(title); mCondition = condition; } - public Item(@StringRes int title) { + protected Item(@StringRes int title) { mTitle = title; } @@ -78,16 +73,11 @@ private boolean isConditionMet() { public abstract void onClick(); } - - public static void show(@NonNull Activity activity, @NonNull Item item, @Nullable Item... items) { - show(activity, null, item, items); - } - - public static void show(@NonNull Activity activity, @StringRes int title, @NonNull Item item, @Nullable Item... items) { - show(activity, activity.getString(title), item, items); + public static void show(@NonNull Activity activity, @StringRes int title, @NonNull Item... items) { + show(activity, activity.getString(title), items); } - public static void show(@NonNull Activity activity, @Nullable String title, @NonNull Item item, @Nullable Item... items) { + public static void show(@NonNull Activity activity, @Nullable String title, @NonNull Item... items) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); View layout = LayoutInflater.from(activity).inflate(R.layout.list_dialog, null); @@ -99,14 +89,9 @@ public static void show(@NonNull Activity activity, @Nullable String title, @Non android.R.layout.simple_list_item_1); final List itemList = new ArrayList<>(1); - if (item.isConditionMet()) { - itemList.add(item); - } - if (items != null) { - for (Item it : items) { - if (it.isConditionMet()) { - itemList.add(it); - } + for (Item it : items) { + if (it.isConditionMet()) { + itemList.add(it); } } @@ -136,37 +121,39 @@ public void onItemClick(AdapterView parent, View view, int position, long id) }); } - public static void showEditText(@NonNull Activity activity, @StringRes int title, - @StringRes int hint, @StringRes int action, + public static void showEditText(@NonNull Activity activity, + @StringRes int title, + @StringRes int hint, + @StringRes int action, @NonNull final EditorListener listener) { showEditText(activity, title, hint, null, action, listener); } - public static void showEditText(@NonNull Activity activity, @StringRes int title, - @StringRes int hint, @Nullable String currentText, - @StringRes int action, @NonNull final EditorListener listener) { - final AlertDialog.Builder editorDialog = new AlertDialog.Builder(activity); - editorDialog.setTitle(title); - final EditText editText = new EditText(activity); + public static void showEditText(@NonNull Activity activity, + @StringRes int title, + @StringRes int hint, + @Nullable String currentText, + @StringRes int action, + @NonNull final EditorListener listener) { + View dialogView = LayoutInflater.from(activity).inflate(R.layout.dialog_edit_text, null); + final EditText editText = (EditText) dialogView.findViewById(R.id.dialog_edit_text); + editText.setHint(hint); if (currentText != null) { editText.setText(currentText); } - editText.setSingleLine(); - LinearLayout layout = new LinearLayout(activity); - layout.setOrientation(LinearLayout.VERTICAL); - int padding = Utils.dpToPx(10); - layout.setPadding(padding, padding, padding, padding); - layout.addView(editText); - editorDialog.setView(layout); - editorDialog.setPositiveButton(action, - new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - listener.onClick(editText.getText().toString()); - } - }); + + final AlertDialog.Builder editorDialog = new AlertDialog.Builder(activity) + .setTitle(title) + .setView(dialogView) + .setPositiveButton(action, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + listener.onClick(editText.getText().toString()); + } + }); Dialog dialog = editorDialog.show(); setDialogSize(activity, dialog); @@ -179,7 +166,10 @@ public static void setDialogSize(@NonNull Context context, @NonNull Dialog dialo if (maxWidth > screenSize - 2 * padding) { maxWidth = screenSize - 2 * padding; } - dialog.getWindow().setLayout(maxWidth, ViewGroup.LayoutParams.WRAP_CONTENT); + Window window = dialog.getWindow(); + if (window != null) { + window.setLayout(maxWidth, ViewGroup.LayoutParams.WRAP_CONTENT); + } } } diff --git a/app/src/main/java/acr/browser/lightning/dialog/LightningDialogBuilder.java b/app/src/main/java/acr/browser/lightning/dialog/LightningDialogBuilder.java index 5d45ad2a4..ee4c0dd30 100644 --- a/app/src/main/java/acr/browser/lightning/dialog/LightningDialogBuilder.java +++ b/app/src/main/java/acr/browser/lightning/dialog/LightningDialogBuilder.java @@ -5,15 +5,19 @@ import android.content.DialogInterface; import android.net.Uri; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; import android.text.TextUtils; +import android.util.Log; import android.view.View; +import android.webkit.URLUtil; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.EditText; +import com.anthonycr.bonsai.CompletableOnSubscribe; import com.anthonycr.bonsai.Schedulers; -import com.squareup.otto.Bus; +import com.anthonycr.bonsai.SingleOnSubscribe; import java.util.List; @@ -22,14 +26,17 @@ import acr.browser.lightning.R; import acr.browser.lightning.activity.MainActivity; import acr.browser.lightning.app.BrowserApp; -import acr.browser.lightning.bus.BookmarkEvents; -import acr.browser.lightning.bus.BrowserEvents; import acr.browser.lightning.constant.BookmarkPage; import acr.browser.lightning.constant.Constants; -import acr.browser.lightning.database.BookmarkManager; -import acr.browser.lightning.database.HistoryDatabase; +import acr.browser.lightning.controller.UIController; import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.database.bookmark.BookmarkModel; +import acr.browser.lightning.database.downloads.DownloadItem; +import acr.browser.lightning.database.downloads.DownloadsModel; +import acr.browser.lightning.database.history.HistoryModel; import acr.browser.lightning.preference.PreferenceManager; +import acr.browser.lightning.utils.IntentUtils; +import acr.browser.lightning.utils.Preconditions; import acr.browser.lightning.utils.Utils; /** @@ -38,11 +45,17 @@ * Created by Stefano Pacifici on 02/09/15, based on Anthony C. Restaino's code. */ public class LightningDialogBuilder { + private static final String TAG = "LightningDialogBuilder"; - @Inject BookmarkManager mBookmarkManager; + public enum NewTab { + FOREGROUND, + BACKGROUND, + INCOGNITO + } + + @Inject BookmarkModel mBookmarkManager; + @Inject DownloadsModel mDownloadsModel; @Inject PreferenceManager mPreferenceManager; - @Inject HistoryDatabase mHistoryDatabase; - @Inject Bus mEventBus; @Inject public LightningDialogBuilder() { @@ -53,10 +66,12 @@ public LightningDialogBuilder() { * Show the appropriated dialog for the long pressed link. It means that we try to understand * if the link is relative to a bookmark or is just a folder. * - * @param context used to show the dialog - * @param url the long pressed url + * @param activity used to show the dialog + * @param url the long pressed url */ - public void showLongPressedDialogForBookmarkUrl(@NonNull final Activity context, @NonNull final String url) { + public void showLongPressedDialogForBookmarkUrl(@NonNull final Activity activity, + @NonNull final UIController uiController, + @NonNull final String url) { final HistoryItem item; if (url.startsWith(Constants.FILE) && url.endsWith(BookmarkPage.FILENAME)) { // TODO hacky, make a better bookmark mechanism in the future @@ -68,36 +83,48 @@ public void showLongPressedDialogForBookmarkUrl(@NonNull final Activity context, item.setTitle(folderTitle); item.setImageId(R.drawable.ic_folder); item.setUrl(Constants.FOLDER + folderTitle); + showBookmarkFolderLongPressedDialog(activity, uiController, item); } else { - item = mBookmarkManager.findBookmarkForUrl(url); - } - if (item != null) { - if (item.isFolder()) { - showBookmarkFolderLongPressedDialog(context, item); - } else { - showLongPressedDialogForBookmarkUrl(context, item); - } + mBookmarkManager.findBookmarkForUrl(url) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable HistoryItem historyItem) { + if (historyItem != null) { + showLongPressedDialogForBookmarkUrl(activity, uiController, historyItem); + } + } + }); } } - public void showLongPressedDialogForBookmarkUrl(@NonNull final Activity activity, @NonNull final HistoryItem item) { + public void showLongPressedDialogForBookmarkUrl(@NonNull final Activity activity, + @NonNull final UIController uiController, + @NonNull final HistoryItem item) { BrowserDialog.show(activity, R.string.action_bookmarks, new BrowserDialog.Item(R.string.dialog_open_new_tab) { @Override public void onClick() { - mEventBus.post(new BrowserEvents.OpenUrlInNewTab(item.getUrl(), BrowserEvents.OpenUrlInNewTab.Location.NEW_TAB)); + uiController.handleNewTab(NewTab.FOREGROUND, item.getUrl()); } }, new BrowserDialog.Item(R.string.dialog_open_background_tab) { @Override public void onClick() { - mEventBus.post(new BrowserEvents.OpenUrlInNewTab(item.getUrl(), BrowserEvents.OpenUrlInNewTab.Location.BACKGROUND)); + uiController.handleNewTab(NewTab.BACKGROUND, item.getUrl()); } }, new BrowserDialog.Item(R.string.dialog_open_incognito_tab, activity instanceof MainActivity) { @Override public void onClick() { - mEventBus.post(new BrowserEvents.OpenUrlInNewTab(item.getUrl(), BrowserEvents.OpenUrlInNewTab.Location.INCOGNITO)); + uiController.handleNewTab(NewTab.INCOGNITO, item.getUrl()); + } + }, + new BrowserDialog.Item(R.string.action_share) { + @Override + public void onClick() { + new IntentUtils(activity).shareUrl(item.getUrl(), item.getTitle()); } }, new BrowserDialog.Item(R.string.dialog_copy_link) { @@ -109,20 +136,58 @@ public void onClick() { new BrowserDialog.Item(R.string.dialog_remove_bookmark) { @Override public void onClick() { - if (mBookmarkManager.deleteBookmark(item)) { - mEventBus.post(new BookmarkEvents.Deleted(item)); - } + mBookmarkManager.deleteBookmark(item) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable Boolean success) { + Preconditions.checkNonNull(success); + if (success) { + uiController.handleBookmarkDeleted(item); + } + } + }); } }, new BrowserDialog.Item(R.string.dialog_edit_bookmark) { @Override public void onClick() { - showEditBookmarkDialog(activity, item); + showEditBookmarkDialog(activity, uiController, item); + } + }); + } + + /** + * Show the appropriated dialog for the long pressed link. + * + * @param activity used to show the dialog + * @param url the long pressed url + */ + public void showLongPressedDialogForDownloadUrl(@NonNull final Activity activity, + @NonNull final UIController uiController, + @NonNull final String url) { + + BrowserDialog.show(activity, R.string.action_downloads, + new BrowserDialog.Item(R.string.dialog_delete_all_downloads) { + @Override + public void onClick() { + mDownloadsModel.deleteAllDownloads() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onComplete() { + uiController.handleDownloadDeleted(); + } + }); } }); } - private void showEditBookmarkDialog(@NonNull final Activity activity, @NonNull final HistoryItem item) { + private void showEditBookmarkDialog(@NonNull final Activity activity, + @NonNull final UIController uiController, + @NonNull final HistoryItem item) { final AlertDialog.Builder editBookmarkDialog = new AlertDialog.Builder(activity); editBookmarkDialog.setTitle(R.string.title_edit_bookmark); final View dialogLayout = View.inflate(activity, R.layout.dialog_edit_bookmark, null); @@ -134,54 +199,81 @@ private void showEditBookmarkDialog(@NonNull final Activity activity, @NonNull f (AutoCompleteTextView) dialogLayout.findViewById(R.id.bookmark_folder); getFolder.setHint(R.string.folder); getFolder.setText(item.getFolder()); - final List folders = mBookmarkManager.getFolderTitles(); - final ArrayAdapter suggestionsAdapter = new ArrayAdapter<>(activity, - android.R.layout.simple_dropdown_item_1line, folders); - getFolder.setThreshold(1); - getFolder.setAdapter(suggestionsAdapter); - editBookmarkDialog.setView(dialogLayout); - editBookmarkDialog.setPositiveButton(activity.getString(R.string.action_ok), - new DialogInterface.OnClickListener() { + mBookmarkManager.getFolderNames() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe>() { @Override - public void onClick(DialogInterface dialog, int which) { - HistoryItem editedItem = new HistoryItem(); - editedItem.setTitle(getTitle.getText().toString()); - editedItem.setUrl(getUrl.getText().toString()); - editedItem.setUrl(getUrl.getText().toString()); - editedItem.setFolder(getFolder.getText().toString()); - mBookmarkManager.editBookmark(item, editedItem); - mEventBus.post(new BookmarkEvents.BookmarkChanged(item, editedItem)); + public void onItem(@Nullable List folders) { + Preconditions.checkNonNull(folders); + final ArrayAdapter suggestionsAdapter = new ArrayAdapter<>(activity, + android.R.layout.simple_dropdown_item_1line, folders); + getFolder.setThreshold(1); + getFolder.setAdapter(suggestionsAdapter); + editBookmarkDialog.setView(dialogLayout); + editBookmarkDialog.setPositiveButton(activity.getString(R.string.action_ok), + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + HistoryItem editedItem = new HistoryItem(); + editedItem.setTitle(getTitle.getText().toString()); + editedItem.setUrl(getUrl.getText().toString()); + editedItem.setUrl(getUrl.getText().toString()); + editedItem.setFolder(getFolder.getText().toString()); + mBookmarkManager.editBookmark(item, editedItem) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onComplete() { + uiController.handleBookmarksChange(); + } + }); + } + }); + Dialog dialog = editBookmarkDialog.show(); + BrowserDialog.setDialogSize(activity, dialog); } }); - Dialog dialog = editBookmarkDialog.show(); - BrowserDialog.setDialogSize(activity, dialog); } - public void showBookmarkFolderLongPressedDialog(@NonNull final Activity activity, @NonNull final HistoryItem item) { + public void showBookmarkFolderLongPressedDialog(@NonNull final Activity activity, + @NonNull final UIController uiController, + @NonNull final HistoryItem item) { BrowserDialog.show(activity, R.string.action_folder, new BrowserDialog.Item(R.string.dialog_rename_folder) { @Override public void onClick() { - showRenameFolderDialog(activity, item); + showRenameFolderDialog(activity, uiController, item); } }, new BrowserDialog.Item(R.string.dialog_remove_folder) { @Override public void onClick() { - mBookmarkManager.deleteFolder(item.getTitle()); - mEventBus.post(new BookmarkEvents.Deleted(item)); + mBookmarkManager.deleteFolder(item.getTitle()) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onComplete() { + uiController.handleBookmarkDeleted(item); + } + }); } }); } - private void showRenameFolderDialog(@NonNull final Activity activity, @NonNull final HistoryItem item) { + private void showRenameFolderDialog(@NonNull final Activity activity, + @NonNull final UIController uiController, + @NonNull final HistoryItem item) { BrowserDialog.showEditText(activity, R.string.title_rename_folder, R.string.hint_title, item.getTitle(), R.string.action_ok, new BrowserDialog.EditorListener() { @Override - public void onClick(String text) { + public void onClick(@NonNull String text) { if (!TextUtils.isEmpty(text)) { final String oldTitle = item.getTitle(); final HistoryItem editedItem = new HistoryItem(); @@ -189,31 +281,46 @@ public void onClick(String text) { editedItem.setUrl(Constants.FOLDER + text); editedItem.setFolder(item.getFolder()); editedItem.setIsFolder(true); - mBookmarkManager.renameFolder(oldTitle, text); - mEventBus.post(new BookmarkEvents.BookmarkChanged(item, editedItem)); + mBookmarkManager.renameFolder(oldTitle, text) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onComplete() { + uiController.handleBookmarksChange(); + } + }); } } }); } - public void showLongPressedHistoryLinkDialog(@NonNull final Activity activity, @NonNull final String url) { + public void showLongPressedHistoryLinkDialog(@NonNull final Activity activity, + @NonNull final UIController uiController, + @NonNull final String url) { BrowserDialog.show(activity, R.string.action_history, new BrowserDialog.Item(R.string.dialog_open_new_tab) { @Override public void onClick() { - mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url)); + uiController.handleNewTab(NewTab.FOREGROUND, url); } }, new BrowserDialog.Item(R.string.dialog_open_background_tab) { @Override public void onClick() { - mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url, BrowserEvents.OpenUrlInNewTab.Location.BACKGROUND)); + uiController.handleNewTab(NewTab.BACKGROUND, url); } }, new BrowserDialog.Item(R.string.dialog_open_incognito_tab, activity instanceof MainActivity) { @Override public void onClick() { - mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url, BrowserEvents.OpenUrlInNewTab.Location.INCOGNITO)); + uiController.handleNewTab(NewTab.INCOGNITO, url); + } + }, + new BrowserDialog.Item(R.string.action_share) { + @Override + public void onClick() { + new IntentUtils(activity).shareUrl(url, null); } }, new BrowserDialog.Item(R.string.dialog_copy_link) { @@ -225,43 +332,47 @@ public void onClick() { new BrowserDialog.Item(R.string.dialog_remove_from_history) { @Override public void onClick() { - BrowserApp.getIOThread().execute(new Runnable() { - @Override - public void run() { - mHistoryDatabase.deleteHistoryItem(url); - // openHistory(); - Schedulers.main().execute(new Runnable() { - @Override - public void run() { - mEventBus.post(new BrowserEvents.OpenHistoryInCurrentTab()); - } - }); - } - }); + HistoryModel.deleteHistoryItem(url) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onComplete() { + uiController.handleHistoryChange(); + } + }); } }); } // TODO There should be a way in which we do not need an activity reference to dowload a file - public void showLongPressImageDialog(@NonNull final Activity activity, @NonNull final String url, + public void showLongPressImageDialog(@NonNull final Activity activity, + @NonNull final UIController uiController, + @NonNull final String url, @NonNull final String userAgent) { BrowserDialog.show(activity, url.replace(Constants.HTTP, ""), new BrowserDialog.Item(R.string.dialog_open_new_tab) { @Override public void onClick() { - mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url)); + uiController.handleNewTab(NewTab.FOREGROUND, url); } }, new BrowserDialog.Item(R.string.dialog_open_background_tab) { @Override public void onClick() { - mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url, BrowserEvents.OpenUrlInNewTab.Location.BACKGROUND)); + uiController.handleNewTab(NewTab.BACKGROUND, url); } }, new BrowserDialog.Item(R.string.dialog_open_incognito_tab, activity instanceof MainActivity) { @Override public void onClick() { - mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url, BrowserEvents.OpenUrlInNewTab.Location.INCOGNITO)); + uiController.handleNewTab(NewTab.INCOGNITO, url); + } + }, + new BrowserDialog.Item(R.string.action_share) { + @Override + public void onClick() { + new IntentUtils(activity).shareUrl(url, null); } }, new BrowserDialog.Item(R.string.dialog_copy_link) { @@ -274,28 +385,45 @@ public void onClick() { @Override public void onClick() { Utils.downloadFile(activity, mPreferenceManager, url, userAgent, "attachment"); + + mDownloadsModel.addDownloadIfNotExists(new DownloadItem(url, URLUtil.guessFileName(url, null, null), "")) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable Boolean item) { + if (item != null && !item) + Log.i(TAG, "error saving download to database"); + } + }); } }); } - public void showLongPressLinkDialog(@NonNull final Activity activity, final String url) { + public void showLongPressLinkDialog(@NonNull final Activity activity, + @NonNull final UIController uiController, + @NonNull final String url) { BrowserDialog.show(activity, url, new BrowserDialog.Item(R.string.dialog_open_new_tab) { @Override public void onClick() { - mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url)); + uiController.handleNewTab(NewTab.FOREGROUND, url); } }, new BrowserDialog.Item(R.string.dialog_open_background_tab) { @Override public void onClick() { - mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url, BrowserEvents.OpenUrlInNewTab.Location.BACKGROUND)); + uiController.handleNewTab(NewTab.BACKGROUND, url); } }, new BrowserDialog.Item(R.string.dialog_open_incognito_tab, activity instanceof MainActivity) { @Override public void onClick() { - mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url, BrowserEvents.OpenUrlInNewTab.Location.INCOGNITO)); + uiController.handleNewTab(NewTab.INCOGNITO, url); + } + }, + new BrowserDialog.Item(R.string.action_share) { + @Override + public void onClick() { + new IntentUtils(activity).shareUrl(url, null); } }, new BrowserDialog.Item(R.string.dialog_copy_link) { diff --git a/app/src/main/java/acr/browser/lightning/download/DownloadHandler.java b/app/src/main/java/acr/browser/lightning/download/DownloadHandler.java index ede6725e0..6f02b92dc 100644 --- a/app/src/main/java/acr/browser/lightning/download/DownloadHandler.java +++ b/app/src/main/java/acr/browser/lightning/download/DownloadHandler.java @@ -23,15 +23,12 @@ import android.webkit.MimeTypeMap; import android.webkit.URLUtil; -import com.squareup.otto.Bus; - import java.io.File; import java.io.IOException; import acr.browser.lightning.BuildConfig; import acr.browser.lightning.R; import acr.browser.lightning.activity.MainActivity; -import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.constant.Constants; import acr.browser.lightning.dialog.BrowserDialog; import acr.browser.lightning.preference.PreferenceManager; @@ -42,22 +39,14 @@ */ public class DownloadHandler { - private static final String TAG = DownloadHandler.class.getSimpleName(); + private static final String TAG = "DownloadHandler"; + private static final String COOKIE_REQUEST_HEADER = "Cookie"; public static final String DEFAULT_DOWNLOAD_PATH = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) .getPath(); - @Nullable - public static String guessFileExtension(@NonNull String filename) { - int lastIndex = filename.lastIndexOf('.') + 1; - if (lastIndex > 0 && filename.length() > lastIndex) { - return filename.substring(lastIndex, filename.length()); - } - return null; - } - /** * Notify the host application a download should be done, or that the data * should be streamed if a streaming viewer is available. @@ -157,7 +146,6 @@ private static String encodePath(@NonNull String path) { private static void onDownloadStartNoStream(@NonNull final Activity context, @NonNull PreferenceManager preferences, String url, String userAgent, String contentDisposition, @Nullable String mimetype) { - final Bus eventBus = BrowserApp.getBus(context); final String filename = URLUtil.guessFileName(url, contentDisposition, mimetype); // Check to see if we have an SDCard @@ -225,7 +213,7 @@ private static void onDownloadStartNoStream(@NonNull final Activity context, @No Utils.showSnackbar(context, R.string.problem_location_download); return; } - String newMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(guessFileExtension(filename)); + String newMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(Utils.guessFileExtension(filename)); Log.d(TAG, "New mimetype: " + newMimeType); request.setMimeType(newMimeType); request.setDestinationUri(Uri.parse(Constants.FILE + location + filename)); diff --git a/app/src/main/java/acr/browser/lightning/download/FetchUrlMimeType.java b/app/src/main/java/acr/browser/lightning/download/FetchUrlMimeType.java index c07362c29..96b707816 100644 --- a/app/src/main/java/acr/browser/lightning/download/FetchUrlMimeType.java +++ b/app/src/main/java/acr/browser/lightning/download/FetchUrlMimeType.java @@ -13,14 +13,12 @@ import android.webkit.URLUtil; import com.anthonycr.bonsai.Schedulers; -import com.squareup.otto.Bus; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import acr.browser.lightning.R; -import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.utils.Utils; /** @@ -33,7 +31,7 @@ */ class FetchUrlMimeType extends Thread { - private static final String TAG = FetchUrlMimeType.class.getSimpleName(); + private static final String TAG = "FetchUrlMimeType"; private final Activity mContext; private final DownloadManager.Request mRequest; @@ -54,7 +52,6 @@ public FetchUrlMimeType(Activity context, DownloadManager.Request request, Strin public void run() { // User agent is likely to be null, though the AndroidHttpClient // seems ok with that. - final Bus eventBus = BrowserApp.getBus(mContext); String mimeType = null; String contentDisposition = null; HttpURLConnection connection = null; @@ -96,7 +93,7 @@ public void run() { if (mimeType.equalsIgnoreCase("text/plain") || mimeType.equalsIgnoreCase("application/octet-stream")) { String newMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( - DownloadHandler.guessFileExtension(mUri)); + Utils.guessFileExtension(mUri)); if (newMimeType != null) { mRequest.setMimeType(newMimeType); } diff --git a/app/src/main/java/acr/browser/lightning/download/LightningDownloadListener.java b/app/src/main/java/acr/browser/lightning/download/LightningDownloadListener.java index 47768e0e5..a38bb93cd 100644 --- a/app/src/main/java/acr/browser/lightning/download/LightningDownloadListener.java +++ b/app/src/main/java/acr/browser/lightning/download/LightningDownloadListener.java @@ -7,17 +7,21 @@ import android.app.Activity; import android.app.Dialog; import android.content.DialogInterface; +import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; +import android.text.format.Formatter; import android.util.Log; import android.webkit.DownloadListener; import android.webkit.URLUtil; import acr.browser.lightning.R; import acr.browser.lightning.app.BrowserApp; -import acr.browser.lightning.constant.Constants; +import acr.browser.lightning.database.downloads.DownloadItem; +import acr.browser.lightning.database.downloads.DownloadsModel; import acr.browser.lightning.dialog.BrowserDialog; import acr.browser.lightning.preference.PreferenceManager; +import com.anthonycr.bonsai.SingleOnSubscribe; import com.anthonycr.grant.PermissionsManager; import com.anthonycr.grant.PermissionsResultAction; @@ -25,10 +29,14 @@ public class LightningDownloadListener implements DownloadListener { + private static final String TAG = "LightningDownloader"; + private final Activity mActivity; @Inject PreferenceManager mPreferenceManager; + @Inject DownloadsModel downloadsModel; + public LightningDownloadListener(Activity context) { BrowserApp.getAppComponent().inject(this); mActivity = context; @@ -36,13 +44,21 @@ public LightningDownloadListener(Activity context) { @Override public void onDownloadStart(final String url, final String userAgent, - final String contentDisposition, final String mimetype, long contentLength) { + final String contentDisposition, final String mimetype, final long contentLength) { PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(mActivity, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, new PermissionsResultAction() { @Override public void onGranted() { - String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype); + final String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype); + final String downloadSize; + + if (contentLength > 0) { + downloadSize = Formatter.formatFileSize(mActivity, contentLength); + } else { + downloadSize = mActivity.getString(R.string.unknown_size); + } + DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -50,6 +66,15 @@ public void onClick(DialogInterface dialog, int which) { case DialogInterface.BUTTON_POSITIVE: DownloadHandler.onDownloadStart(mActivity, mPreferenceManager, url, userAgent, contentDisposition, mimetype); + + downloadsModel.addDownloadIfNotExists(new DownloadItem(url, fileName, downloadSize)) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable Boolean item) { + if (item != null && !item) + Log.i(TAG, "error saving download to database"); + } + }); break; case DialogInterface.BUTTON_NEGATIVE: break; @@ -58,14 +83,15 @@ public void onClick(DialogInterface dialog, int which) { }; AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); // dialog + String message = mActivity.getString(R.string.dialog_download, downloadSize); Dialog dialog = builder.setTitle(fileName) - .setMessage(mActivity.getResources().getString(R.string.dialog_download)) + .setMessage(message) .setPositiveButton(mActivity.getResources().getString(R.string.action_download), dialogClickListener) .setNegativeButton(mActivity.getResources().getString(R.string.action_cancel), dialogClickListener).show(); BrowserDialog.setDialogSize(mActivity, dialog); - Log.i(Constants.TAG, "Downloading: " + fileName); + Log.i(TAG, "Downloading: " + fileName); } @Override diff --git a/app/src/main/java/acr/browser/lightning/favicon/FaviconModel.java b/app/src/main/java/acr/browser/lightning/favicon/FaviconModel.java new file mode 100644 index 000000000..e9ecf6c80 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/favicon/FaviconModel.java @@ -0,0 +1,217 @@ +package acr.browser.lightning.favicon; + +import android.app.Application; +import android.graphics.Bitmap; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; +import android.util.LruCache; + +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableAction; +import com.anthonycr.bonsai.CompletableSubscriber; +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleSubscriber; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import acr.browser.lightning.utils.FileUtils; +import acr.browser.lightning.utils.Preconditions; +import acr.browser.lightning.utils.Utils; + +/** + * Reactive model that can fetch favicons + * from URLs and also cache them. + */ +@Singleton +public class FaviconModel { + + private static final String TAG = "FaviconModel"; + + @NonNull private final ImageFetcher mImageFetcher; + @NonNull private final Application mApplication; + @NonNull private final LruCache mFaviconCache = new LruCache((int) FileUtils.megabytesToBytes(1)) { + @Override + protected int sizeOf(String key, Bitmap value) { + return value.getByteCount(); + } + }; + + @Inject + FaviconModel(@NonNull Application application) { + mImageFetcher = new ImageFetcher(); + mApplication = application; + } + + /** + * Retrieves a favicon from the memory cache. + * Bitmap may not be present if no bitmap has + * been added for the URL or if it has been + * evicted from the memory cache. + * + * @param url the URL to retrieve the bitmap for. + * @return the bitmap associated with the URL, + * may be null. + */ + @Nullable + private Bitmap getFaviconFromMemCache(@NonNull String url) { + Preconditions.checkNonNull(url); + synchronized (mFaviconCache) { + return mFaviconCache.get(url); + } + } + + /** + * Adds a bitmap to the memory cache + * for the given URL. + * + * @param url the URL to map the bitmap to. + * @param bitmap the bitmap to store. + */ + private void addFaviconToMemCache(@NonNull String url, @NonNull Bitmap bitmap) { + Preconditions.checkNonNull(url); + Preconditions.checkNonNull(bitmap); + synchronized (mFaviconCache) { + mFaviconCache.put(url, bitmap); + } + } + + /** + * Creates the cache file for the favicon + * image. File name will be in the form of + * [hash of URI host].png + * + * @param app the context needed to retrieve the + * cache directory. + * @param uri the URI to use as a unique identifier. + * @return a valid cache file. + */ + @NonNull + private static File createFaviconCacheFile(@NonNull Application app, @NonNull Uri uri) { + FaviconUtils.assertUriSafe(uri); + + String hash = String.valueOf(uri.getHost().hashCode()); + + return new File(app.getCacheDir(), hash + ".png"); + } + + /** + * Retrieves the favicon for a URL, + * may be from network or cache. + * + * @param url the URL that we should retrieve the + * favicon for. + * @param defaultFavicon the default favicon if no + * favicon is found. + * @param allowGoogleService true to allow grabbing favicons + * from Google, false otherwise. + * @return an observable that emits a bitmap if one is found, + * or the default if none was found. + */ + @NonNull + public Single faviconForUrl(@NonNull final String url, + @NonNull final Bitmap defaultFavicon, + final boolean allowGoogleService) { + return Single.create(new SingleAction() { + @Override + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + Uri uri = FaviconUtils.safeUri(url); + + if (uri == null) { + + Bitmap newFavicon = Utils.padFavicon(defaultFavicon); + + subscriber.onItem(newFavicon); + subscriber.onComplete(); + + return; + } + + File faviconCacheFile = createFaviconCacheFile(mApplication, uri); + + + Bitmap favicon = getFaviconFromMemCache(url); + + if (faviconCacheFile.exists() && favicon == null) { + favicon = mImageFetcher.retrieveFaviconFromCache(faviconCacheFile); + } + + if (favicon == null) { + // TODO: 6/5/17 figure out if optimistic favicon retrieval should be added back or dropped + // favicon = mImageFetcher.retrieveBitmapFromDomain(uri); + } else { + Bitmap newFavicon = Utils.padFavicon(favicon); + + subscriber.onItem(newFavicon); + subscriber.onComplete(); + + return; + } + + // if (favicon == null && allowGoogleService) { + // favicon = mImageFetcher.retrieveBitmapFromGoogle(uri); + // } + + // if (favicon != null) { + // addFaviconToMemCache(url, favicon); + // cacheFaviconForUrl(favicon, url).subscribe(); + // } + + // if (favicon == null) { + favicon = defaultFavicon; + // } + + Bitmap newFavicon = Utils.padFavicon(favicon); + + subscriber.onItem(newFavicon); + subscriber.onComplete(); + } + }); + } + + /** + * Caches a favicon for a particular URL. + * + * @param favicon the favicon to cache. + * @param url the URL to cache the favicon for. + * @return an observable that notifies the consumer + * when it is complete. + */ + @NonNull + public Completable cacheFaviconForUrl(@NonNull final Bitmap favicon, + @NonNull final String url) { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + Uri uri = FaviconUtils.safeUri(url); + + if (uri == null) { + subscriber.onComplete(); + return; + } + + Log.d(TAG, "Caching icon for " + uri.getHost()); + FileOutputStream fos = null; + + try { + File image = createFaviconCacheFile(mApplication, uri); + fos = new FileOutputStream(image); + favicon.compress(Bitmap.CompressFormat.PNG, 100, fos); + fos.flush(); + } catch (IOException e) { + Log.e(TAG, "Unable to cache favicon", e); + } finally { + Utils.close(fos); + } + } + }); + } + +} diff --git a/app/src/main/java/acr/browser/lightning/favicon/FaviconUtils.java b/app/src/main/java/acr/browser/lightning/favicon/FaviconUtils.java new file mode 100644 index 000000000..a32420b0f --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/favicon/FaviconUtils.java @@ -0,0 +1,32 @@ +package acr.browser.lightning.favicon; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +/** + * Simple utils for favicon fetching. + */ +class FaviconUtils { + @Nullable + static Uri safeUri(@NonNull String url) { + if (TextUtils.isEmpty(url)) { + return null; + } + + Uri uri = Uri.parse(url); + + if (TextUtils.isEmpty(uri.getScheme()) || TextUtils.isEmpty(uri.getHost())) { + return null; + } + + return uri; + } + + static void assertUriSafe(@Nullable Uri uri) { + if (uri == null || TextUtils.isEmpty(uri.getScheme()) || TextUtils.isEmpty(uri.getHost())) { + throw new RuntimeException("Unsafe uri provided"); + } + } +} diff --git a/app/src/main/java/acr/browser/lightning/favicon/ImageFetcher.java b/app/src/main/java/acr/browser/lightning/favicon/ImageFetcher.java new file mode 100644 index 000000000..337e40844 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/favicon/ImageFetcher.java @@ -0,0 +1,110 @@ +package acr.browser.lightning.favicon; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import acr.browser.lightning.utils.Utils; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * An image fetcher that creates image + * loading requests on demand. + */ +class ImageFetcher { + + private static final String TAG = "ImageFetcher"; + + @NonNull private final BitmapFactory.Options mLoaderOptions = new BitmapFactory.Options(); + @NonNull private final OkHttpClient mHttpClient = new OkHttpClient(); + + ImageFetcher() {} + + @Nullable + Bitmap retrieveFaviconFromCache(@NonNull File cacheFile) { + return BitmapFactory.decodeFile(cacheFile.getPath(), mLoaderOptions); + } + + @Nullable + Bitmap retrieveBitmapFromDomain(@NonNull Uri uri) { + FaviconUtils.assertUriSafe(uri); + + String faviconUrlGuess = uri.getScheme() + "://" + uri.getHost() + "/favicon.ico"; + + return retrieveBitmapFromUrl(faviconUrlGuess); + } + + @Nullable + Bitmap retrieveBitmapFromGoogle(@NonNull Uri uri) { + FaviconUtils.assertUriSafe(uri); + + String googleFaviconUrl = "https://www.google.com/s2/favicons?domain_url=" + uri.getHost(); + + return retrieveBitmapFromUrl(googleFaviconUrl); + } + + @Nullable + private Bitmap retrieveBitmapFromUrl(@NonNull String url) { + Bitmap icon = null; + + InputStream boundsStream = null; + InputStream iconStream = null; + + try { + mLoaderOptions.inSampleSize = 1; + mLoaderOptions.inJustDecodeBounds = true; + + Request imageRequest = new Request.Builder().url(url).build(); + + Response boundsResponse = mHttpClient.newCall(imageRequest).execute(); + ResponseBody boundsBody = boundsResponse.body(); + + if (boundsBody == null) { + return null; + } + + boundsStream = boundsBody.byteStream(); + + BitmapFactory.decodeStream(boundsStream, null, mLoaderOptions); + + boundsBody.close(); + + int size = Utils.dpToPx(24); + + mLoaderOptions.inSampleSize = Utils.calculateInSampleSize(mLoaderOptions, size, size); + mLoaderOptions.inJustDecodeBounds = false; + + Response imageResponse = mHttpClient.newCall(imageRequest).execute(); + + ResponseBody imageBody = imageResponse.body(); + + if (imageBody == null) { + return null; + } + + iconStream = imageBody.byteStream(); + + icon = BitmapFactory.decodeStream(iconStream, null, mLoaderOptions); + + imageBody.close(); + } catch (IOException exception) { + Log.d(TAG, "Unable to download icon: " + url); + } finally { + Utils.close(boundsStream); + Utils.close(iconStream); + } + + return icon; + } + +} diff --git a/app/src/main/java/acr/browser/lightning/fragment/BookmarkSettingsFragment.java b/app/src/main/java/acr/browser/lightning/fragment/BookmarkSettingsFragment.java index deeb8a421..4e9b8808f 100644 --- a/app/src/main/java/acr/browser/lightning/fragment/BookmarkSettingsFragment.java +++ b/app/src/main/java/acr/browser/lightning/fragment/BookmarkSettingsFragment.java @@ -5,6 +5,7 @@ import android.Manifest; import android.app.Activity; +import android.app.Application; import android.app.Dialog; import android.content.DialogInterface; import android.content.pm.ApplicationInfo; @@ -21,6 +22,9 @@ import android.util.Log; import android.widget.ArrayAdapter; +import com.anthonycr.bonsai.CompletableOnSubscribe; +import com.anthonycr.bonsai.SingleOnSubscribe; +import com.anthonycr.bonsai.Subscription; import com.anthonycr.grant.PermissionsManager; import com.anthonycr.grant.PermissionsResultAction; @@ -35,20 +39,23 @@ import acr.browser.lightning.R; import acr.browser.lightning.app.BrowserApp; -import acr.browser.lightning.constant.Constants; -import acr.browser.lightning.database.BookmarkLocalSync; -import acr.browser.lightning.database.BookmarkLocalSync.Source; -import acr.browser.lightning.database.BookmarkManager; +import acr.browser.lightning.database.bookmark.BookmarkExporter; +import acr.browser.lightning.database.bookmark.BookmarkLocalSync; +import acr.browser.lightning.database.bookmark.BookmarkLocalSync.Source; import acr.browser.lightning.database.HistoryItem; -import com.anthonycr.bonsai.OnSubscribe; + import com.anthonycr.bonsai.Schedulers; +import acr.browser.lightning.database.bookmark.BookmarkModel; import acr.browser.lightning.dialog.BrowserDialog; import acr.browser.lightning.utils.Preconditions; +import acr.browser.lightning.utils.SubscriptionUtils; import acr.browser.lightning.utils.Utils; public class BookmarkSettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener { + private static final String TAG = "BookmarkSettingsFrag"; + private static final String SETTINGS_EXPORT = "export_bookmark"; private static final String SETTINGS_IMPORT = "import_bookmark"; private static final String SETTINGS_IMPORT_BROWSER = "import_browser"; @@ -56,14 +63,20 @@ public class BookmarkSettingsFragment extends PreferenceFragment implements Pref @Nullable private Activity mActivity; - @Inject BookmarkManager mBookmarkManager; + @Inject BookmarkModel mBookmarkManager; + @Inject Application mApplication; + private File[] mFileList; private String[] mFileNameList; + @Nullable private BookmarkLocalSync mSync; + @Nullable private Subscription mImportSubscription; + @Nullable private Subscription mExportSubscription; + private static final String[] REQUIRED_PERMISSIONS = new String[]{ - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE }; private static final File mPath = new File(Environment.getExternalStorageDirectory().toString()); @@ -80,7 +93,7 @@ public ImportBookmarksTask(Activity activity, Source source) { @Override protected Integer doInBackground(Void... params) { List list; - Log.d(Constants.TAG, "Loading bookmarks from: " + mSource.name()); + Log.d(TAG, "Loading bookmarks from: " + mSource.name()); switch (mSource) { case STOCK: list = getSync().getBookmarksFromStockBrowser(); @@ -146,9 +159,21 @@ public void onCreate(Bundle savedInstanceState) { } } + @Override + public void onDestroyView() { + super.onDestroyView(); + + SubscriptionUtils.safeUnsubscribe(mExportSubscription); + SubscriptionUtils.safeUnsubscribe(mImportSubscription); + } + @Override public void onDestroy() { super.onDestroy(); + + SubscriptionUtils.safeUnsubscribe(mExportSubscription); + SubscriptionUtils.safeUnsubscribe(mImportSubscription); + mActivity = null; } @@ -162,20 +187,18 @@ private void initPrefs() { importPref.setOnPreferenceClickListener(this); deletePref.setOnPreferenceClickListener(this); - BrowserApp.getTaskThread().execute(new Runnable() { - @Override - public void run() { - final boolean isBrowserImportSupported = getSync().isBrowserImportSupported(); - Schedulers.main().execute(new Runnable() { - @Override - public void run() { - Preference importStock = findPreference(SETTINGS_IMPORT_BROWSER); - importStock.setEnabled(isBrowserImportSupported); - importStock.setOnPreferenceClickListener(BookmarkSettingsFragment.this); - } - }); - } - }); + getSync().isBrowserImportSupported() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable Boolean supported) { + Preconditions.checkNonNull(supported); + Preference importStock = findPreference(SETTINGS_IMPORT_BROWSER); + importStock.setEnabled(supported); + importStock.setOnPreferenceClickListener(BookmarkSettingsFragment.this); + } + }); } @@ -184,43 +207,89 @@ public boolean onPreferenceClick(@NonNull Preference preference) { switch (preference.getKey()) { case SETTINGS_EXPORT: PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(getActivity(), REQUIRED_PERMISSIONS, - new PermissionsResultAction() { - @Override - public void onGranted() { - mBookmarkManager.exportBookmarks(getActivity()); - } + new PermissionsResultAction() { + @Override + public void onGranted() { + mBookmarkManager.getAllBookmarks() + .subscribeOn(Schedulers.io()) + .subscribe(new SingleOnSubscribe>() { + @Override + public void onItem(@Nullable List item) { + if (!isAdded()) { + return; + } + + Preconditions.checkNonNull(item); + final File exportFile = BookmarkExporter.createNewExportFile(); + SubscriptionUtils.safeUnsubscribe(mExportSubscription); + mExportSubscription = BookmarkExporter.exportBookmarksToFile(item, exportFile) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onComplete() { + mExportSubscription = null; + + Activity activity = getActivity(); + if (activity != null) { + Utils.showSnackbar(activity, activity.getString(R.string.bookmark_export_path) + + ' ' + exportFile.getPath()); + } + } + + @Override + public void onError(@NonNull Throwable throwable) { + mExportSubscription = null; + + Log.e(TAG, "onError: exporting bookmarks", throwable); + Activity activity = getActivity(); + if (activity != null && !activity.isFinishing() && isAdded()) { + Utils.createInformativeDialog(activity, R.string.title_error, R.string.bookmark_export_failure); + } else { + Utils.showToast(mApplication, R.string.bookmark_export_failure); + } + } + }); + } + }); + } - @Override - public void onDenied(String permission) { - //TODO Show message + @Override + public void onDenied(String permission) { + Activity activity = getActivity(); + if (activity != null && !activity.isFinishing() && isAdded()) { + Utils.createInformativeDialog(activity, R.string.title_error, R.string.bookmark_export_failure); + } else { + Utils.showToast(mApplication, R.string.bookmark_export_failure); } - }); + } + }); return true; case SETTINGS_IMPORT: PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(getActivity(), REQUIRED_PERMISSIONS, - new PermissionsResultAction() { - @Override - public void onGranted() { - loadFileList(null); - createDialog(); - } + new PermissionsResultAction() { + @Override + public void onGranted() { + loadFileList(null); + createDialog(); + } - @Override - public void onDenied(String permission) { - //TODO Show message - } - }); + @Override + public void onDenied(String permission) { + //TODO Show message + } + }); return true; case SETTINGS_IMPORT_BROWSER: getSync().getSupportedBrowsers().subscribeOn(Schedulers.worker()) - .observeOn(Schedulers.main()).subscribe(new OnSubscribe>() { + .observeOn(Schedulers.main()).subscribe(new SingleOnSubscribe>() { @Override - public void onNext(@Nullable List items) { + public void onItem(@Nullable List item) { Activity activity = getActivity(); - if (items == null || activity == null) { + if (item == null || activity == null) { return; } - List titles = buildTitleList(activity, items); + List titles = buildTitleList(activity, item); showChooserDialog(activity, titles); } }); @@ -245,7 +314,7 @@ private void showDeleteBookmarksDialog() { builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - mBookmarkManager.deleteAllBookmarks(); + mBookmarkManager.deleteAllBookmarks().subscribeOn(Schedulers.io()).subscribe(); } }); Dialog dialog = builder.show(); @@ -289,7 +358,7 @@ private List buildTitleList(@NonNull Activity activity, @NonNull List list) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); final ArrayAdapter adapter = new ArrayAdapter<>(activity, - android.R.layout.simple_list_item_1); + android.R.layout.simple_list_item_1); for (String title : list) { adapter.add(title); } @@ -299,6 +368,7 @@ private void showChooserDialog(final Activity activity, List list) { @Override public void onClick(DialogInterface dialog, int which) { String title = adapter.getItem(which); + Preconditions.checkNonNull(title); Source source = null; if (title.equals(getString(R.string.stock_browser))) { source = Source.STOCK; @@ -384,7 +454,7 @@ public int compare(@NonNull File a, @NonNull File b) { } private void createDialog() { - if(mActivity == null){ + if (mActivity == null) { return; } final AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); @@ -407,7 +477,44 @@ public void onClick(DialogInterface dialog, int which) { Dialog dialog1 = builder.show(); BrowserDialog.setDialogSize(mActivity, dialog1); } else { - mBookmarkManager.importBookmarksFromFile(mFileList[which], getActivity()); + SubscriptionUtils.safeUnsubscribe(mImportSubscription); + mImportSubscription = BookmarkExporter.importBookmarksFromFile(mFileList[which]) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe>() { + @Override + public void onItem(@Nullable final List importList) { + mImportSubscription = null; + + Preconditions.checkNonNull(importList); + mBookmarkManager.addBookmarkList(importList) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onComplete() { + Activity activity = getActivity(); + if (activity != null) { + String message = activity.getResources().getString(R.string.message_import); + Utils.showSnackbar(activity, importList.size() + " " + message); + } + } + }); + } + + @Override + public void onError(@NonNull Throwable throwable) { + mImportSubscription = null; + + Log.e(TAG, "onError: importing bookmarks", throwable); + Activity activity = getActivity(); + if (activity != null && !activity.isFinishing() && isAdded()) { + Utils.createInformativeDialog(activity, R.string.title_error, R.string.import_bookmark_error); + } else { + Utils.showToast(mApplication, R.string.import_bookmark_error); + } + } + }); } } diff --git a/app/src/main/java/acr/browser/lightning/fragment/BookmarksFragment.java b/app/src/main/java/acr/browser/lightning/fragment/BookmarksFragment.java index 494ce06bb..62776b26b 100644 --- a/app/src/main/java/acr/browser/lightning/fragment/BookmarksFragment.java +++ b/app/src/main/java/acr/browser/lightning/fragment/BookmarksFragment.java @@ -11,105 +11,103 @@ import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.view.ViewCompat; +import android.support.v7.util.DiffUtil; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.animation.AccelerateInterpolator; import android.view.animation.Animation; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.Transformation; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.ArrayAdapter; import android.widget.FrameLayout; import android.widget.ImageView; -import android.widget.ListView; import android.widget.TextView; -import com.anthonycr.bonsai.Action; -import com.anthonycr.bonsai.Observable; -import com.anthonycr.bonsai.OnSubscribe; import com.anthonycr.bonsai.Schedulers; -import com.anthonycr.bonsai.Subscriber; -import com.squareup.otto.Bus; -import com.squareup.otto.Subscribe; +import com.anthonycr.bonsai.SingleOnSubscribe; +import com.anthonycr.bonsai.Subscription; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import javax.inject.Inject; import acr.browser.lightning.R; +import acr.browser.lightning.activity.BookmarkUiModel; import acr.browser.lightning.activity.ReadingActivity; import acr.browser.lightning.activity.TabsManager; +import acr.browser.lightning.animation.AnimationUtils; import acr.browser.lightning.app.BrowserApp; -import acr.browser.lightning.async.AsyncExecutor; -import acr.browser.lightning.async.ImageDownloadTask; import acr.browser.lightning.browser.BookmarksView; -import acr.browser.lightning.bus.BookmarkEvents; import acr.browser.lightning.constant.Constants; import acr.browser.lightning.controller.UIController; -import acr.browser.lightning.database.BookmarkManager; import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.database.bookmark.BookmarkModel; import acr.browser.lightning.dialog.LightningDialogBuilder; +import acr.browser.lightning.favicon.FaviconModel; import acr.browser.lightning.preference.PreferenceManager; +import acr.browser.lightning.utils.Preconditions; +import acr.browser.lightning.utils.SubscriptionUtils; import acr.browser.lightning.utils.ThemeUtils; import acr.browser.lightning.view.LightningView; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.Unbinder; public class BookmarksFragment extends Fragment implements View.OnClickListener, View.OnLongClickListener, BookmarksView { - private final static String TAG = BookmarksFragment.class.getSimpleName(); + @NonNull + public static BookmarksFragment createFragment(boolean isIncognito) { + BookmarksFragment bookmarksFragment = new BookmarksFragment(); + final Bundle bookmarksFragmentArguments = new Bundle(); + bookmarksFragmentArguments.putBoolean(BookmarksFragment.INCOGNITO_MODE, isIncognito); + bookmarksFragment.setArguments(bookmarksFragmentArguments); - public final static String INCOGNITO_MODE = TAG + ".INCOGNITO_MODE"; + return bookmarksFragment; + } - // Managers - @Inject BookmarkManager mBookmarkManager; + private static final String TAG = "BookmarksFragment"; - // Event bus - @Inject Bus mEventBus; + private final static String INCOGNITO_MODE = TAG + ".INCOGNITO_MODE"; + + // Managers + @Inject BookmarkModel mBookmarkManager; // Dialog builder @Inject LightningDialogBuilder mBookmarksDialogBuilder; @Inject PreferenceManager mPreferenceManager; + @Inject FaviconModel mFaviconModel; + private TabsManager mTabsManager; private UIController mUiController; // Adapter - private BookmarkViewAdapter mBookmarkAdapter; + private BookmarkListAdapter mBookmarkAdapter; // Preloaded images private Bitmap mWebpageBitmap, mFolderBitmap; - // Bookmarks - private final List mBookmarks = new ArrayList<>(); - // Views - private ListView mBookmarksListView; - private ImageView mBookmarkTitleImage, mBookmarkImage; + @BindView(R.id.right_drawer_list) RecyclerView mBookmarksListView; + @BindView(R.id.starIcon) ImageView mBookmarkTitleImage; + @BindView(R.id.icon_star) ImageView mBookmarkImage; + + @Nullable private Unbinder mUnbinder; // Colors private int mIconColor, mScrollIndex; private boolean mIsIncognito; - private Observable initBookmarkManager() { - return Observable.create(new Action() { - @Override - public void onSubscribe(@NonNull Subscriber subscriber) { - Context context = getContext(); - if (context != null) { - mBookmarkAdapter = new BookmarkViewAdapter(context, mBookmarks); - setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false); - subscriber.onNext(mBookmarkAdapter); - } - subscriber.onComplete(); - } - }); - } + @Nullable private Subscription mBookmarksSubscription; + @Nullable private Subscription mFoldersSubscription; + @Nullable private Subscription mBookmarkUpdateSubscription; + + @NonNull private final BookmarkUiModel mUiModel = new BookmarkUiModel(); @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -137,11 +135,10 @@ private TabsManager getTabsManager() { // Handle bookmark click private final OnItemClickListener mItemClickListener = new OnItemClickListener() { @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - final HistoryItem item = mBookmarks.get(position); + public void onItemClick(@NonNull HistoryItem item) { if (item.isFolder()) { - mScrollIndex = mBookmarksListView.getFirstVisiblePosition(); - setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(item.getTitle(), true), true); + mScrollIndex = ((LinearLayoutManager) mBookmarksListView.getLayoutManager()).findFirstVisibleItemPosition(); + setBookmarksShown(item.getTitle(), true); } else { mUiController.bookmarkItemClicked(item); } @@ -150,8 +147,7 @@ public void onItemClick(AdapterView parent, View view, int position, long id) private final OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() { @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - final HistoryItem item = mBookmarks.get(position); + public boolean onItemLongClick(@NonNull HistoryItem item) { handleLongPress(item); return true; } @@ -161,7 +157,7 @@ public boolean onItemLongClick(AdapterView parent, View view, int position, l public void onResume() { super.onResume(); if (mBookmarkAdapter != null) { - setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false); + setBookmarksShown(null, false); } } @@ -169,20 +165,15 @@ public void onResume() { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.bookmark_drawer, container, false); - mBookmarksListView = (ListView) view.findViewById(R.id.right_drawer_list); - mBookmarksListView.setOnItemClickListener(mItemClickListener); - mBookmarksListView.setOnItemLongClickListener(mItemLongClickListener); - mBookmarkTitleImage = (ImageView) view.findViewById(R.id.starIcon); + mUnbinder = ButterKnife.bind(this, view); mBookmarkTitleImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN); - mBookmarkImage = (ImageView) view.findViewById(R.id.icon_star); final View backView = view.findViewById(R.id.bookmark_back_button); backView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (mBookmarkManager == null) return; - if (!mBookmarkManager.isRootFolder()) { - setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), true); - mBookmarksListView.setSelection(mScrollIndex); + if (!mUiModel.isRootFolder()) { + setBookmarksShown(null, true); + mBookmarksListView.getLayoutManager().scrollToPosition(mScrollIndex); } } }); @@ -190,27 +181,46 @@ public void onClick(View v) { setupNavigationButton(view, R.id.action_reading, R.id.icon_reading); setupNavigationButton(view, R.id.action_toggle_desktop, R.id.icon_desktop); - initBookmarkManager().subscribeOn(Schedulers.io()) - .observeOn(Schedulers.main()) - .subscribe(new OnSubscribe() { - @Override - public void onNext(@Nullable BookmarkViewAdapter item) { - mBookmarksListView.setAdapter(mBookmarkAdapter); - } - }); + mBookmarkAdapter = new BookmarkListAdapter(mFaviconModel, mFolderBitmap, mWebpageBitmap); + mBookmarkAdapter.setOnItemClickListener(mItemClickListener); + mBookmarkAdapter.setOnItemLongClickListener(mItemLongClickListener); + mBookmarksListView.setLayoutManager(new LinearLayoutManager(getContext())); + mBookmarksListView.setAdapter(mBookmarkAdapter); + + setBookmarksShown(null, true); + return view; } @Override - public void onStart() { - super.onStart(); - mEventBus.register(this); + public void onDestroyView() { + super.onDestroyView(); + + SubscriptionUtils.safeUnsubscribe(mBookmarksSubscription); + SubscriptionUtils.safeUnsubscribe(mFoldersSubscription); + SubscriptionUtils.safeUnsubscribe(mBookmarkUpdateSubscription); + + if (mBookmarkAdapter != null) { + mBookmarkAdapter.cleanupSubscriptions(); + } + + if (mUnbinder != null) { + mUnbinder.unbind(); + mUnbinder = null; + } } @Override - public void onStop() { - super.onStop(); - mEventBus.unregister(this); + public void onDestroy() { + super.onDestroy(); + + SubscriptionUtils.safeUnsubscribe(mBookmarksSubscription); + SubscriptionUtils.safeUnsubscribe(mFoldersSubscription); + SubscriptionUtils.safeUnsubscribe(mBookmarkUpdateSubscription); + + if (mBookmarkAdapter != null) { + mBookmarkAdapter.cleanupSubscriptions(); + } } public void reinitializePreferences() { @@ -226,70 +236,84 @@ public void reinitializePreferences() { } private void updateBookmarkIndicator(final String url) { - if (!mBookmarkManager.isBookmark(url)) { - mBookmarkImage.setImageResource(R.drawable.ic_action_star); - mBookmarkImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN); - } else { - mBookmarkImage.setImageResource(R.drawable.ic_bookmark); - mBookmarkImage.setColorFilter(ThemeUtils.getAccentColor(getContext()), PorterDuff.Mode.SRC_IN); - } + SubscriptionUtils.safeUnsubscribe(mBookmarkUpdateSubscription); + mBookmarkUpdateSubscription = mBookmarkManager.isBookmark(url) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable Boolean item) { + mBookmarkUpdateSubscription = null; + Preconditions.checkNonNull(item); + Activity activity = getActivity(); + if (mBookmarkImage == null || activity == null) { + return; + } + if (!item) { + mBookmarkImage.setImageResource(R.drawable.ic_action_star); + mBookmarkImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN); + } else { + mBookmarkImage.setImageResource(R.drawable.ic_bookmark); + mBookmarkImage.setColorFilter(ThemeUtils.getAccentColor(activity), PorterDuff.Mode.SRC_IN); + } + } + }); } - @Subscribe - public void bookmarkDeleted(@NonNull final BookmarkEvents.Deleted event) { - mBookmarks.remove(event.item); - if (event.item.isFolder()) { - setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false); + @Override + public void handleBookmarkDeleted(@NonNull HistoryItem item) { + if (item.isFolder()) { + setBookmarksShown(null, false); } else { - mBookmarkAdapter.notifyDataSetChanged(); + mBookmarkAdapter.deleteItem(item); } } + private void setBookmarksShown(@Nullable final String folder, final boolean animate) { + SubscriptionUtils.safeUnsubscribe(mBookmarksSubscription); + mBookmarksSubscription = mBookmarkManager.getBookmarksFromFolderSorted(folder) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe>() { + @Override + public void onItem(@Nullable final List item) { + mBookmarksSubscription = null; + Preconditions.checkNonNull(item); + + mUiModel.setCurrentFolder(folder); + if (folder == null) { + SubscriptionUtils.safeUnsubscribe(mFoldersSubscription); + mFoldersSubscription = mBookmarkManager.getFoldersSorted() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe>() { + @Override + public void onItem(@Nullable List folders) { + mFoldersSubscription = null; + Preconditions.checkNonNull(folders); + item.addAll(folders); + setBookmarkDataSet(item, animate); + } + }); + } else { + setBookmarkDataSet(item, animate); + } + } + }); + } + private void setBookmarkDataSet(@NonNull List items, boolean animate) { - mBookmarks.clear(); - mBookmarks.addAll(items); - mBookmarkAdapter.notifyDataSetChanged(); + mBookmarkAdapter.updateItems(items); final int resource; - if (mBookmarkManager.isRootFolder()) { + if (mUiModel.isRootFolder()) { resource = R.drawable.ic_action_star; } else { resource = R.drawable.ic_action_back; } - final Animation startRotation = new Animation() { - @Override - protected void applyTransformation(float interpolatedTime, Transformation t) { - mBookmarkTitleImage.setRotationY(90 * interpolatedTime); - } - }; - final Animation finishRotation = new Animation() { - @Override - protected void applyTransformation(float interpolatedTime, Transformation t) { - mBookmarkTitleImage.setRotationY((-90) + (90 * interpolatedTime)); - } - }; - startRotation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { - } - - @Override - public void onAnimationEnd(Animation animation) { - mBookmarkTitleImage.setImageResource(resource); - mBookmarkTitleImage.startAnimation(finishRotation); - } - - @Override - public void onAnimationRepeat(Animation animation) { - } - }); - startRotation.setInterpolator(new AccelerateInterpolator()); - finishRotation.setInterpolator(new DecelerateInterpolator()); - startRotation.setDuration(250); - finishRotation.setDuration(250); - if (animate) { - mBookmarkTitleImage.startAnimation(startRotation); + Animation transition = AnimationUtils.createRotationTransitionAnimation(mBookmarkTitleImage, resource); + mBookmarkTitleImage.startAnimation(transition); } else { mBookmarkTitleImage.setImageResource(resource); } @@ -305,9 +329,9 @@ private void setupNavigationButton(@NonNull View view, @IdRes int buttonId, @IdR private void handleLongPress(@NonNull final HistoryItem item) { if (item.isFolder()) { - mBookmarksDialogBuilder.showBookmarkFolderLongPressedDialog(getActivity(), item); + mBookmarksDialogBuilder.showBookmarkFolderLongPressedDialog(getActivity(), mUiController, item); } else { - mBookmarksDialogBuilder.showLongPressedDialogForBookmarkUrl(getActivity(), item); + mBookmarksDialogBuilder.showLongPressedDialogForBookmarkUrl(getActivity(), mUiController, item); } } @@ -345,67 +369,200 @@ public boolean onLongClick(View v) { @Override public void navigateBack() { - if (mBookmarkManager.isRootFolder()) { + if (mUiModel.isRootFolder()) { mUiController.closeBookmarksDrawer(); } else { - setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), true); - mBookmarksListView.setSelection(mScrollIndex); + setBookmarksShown(null, true); + mBookmarksListView.getLayoutManager().scrollToPosition(mScrollIndex); } } @Override public void handleUpdatedUrl(@NonNull String url) { updateBookmarkIndicator(url); - String folder = mBookmarkManager.getCurrentFolder(); - setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(folder, true), false); + String folder = mUiModel.getCurrentFolder(); + setBookmarksShown(folder, false); } - private class BookmarkViewAdapter extends ArrayAdapter { + static class BookmarkViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { + @BindView(R.id.textBookmark) TextView txtTitle; + @BindView(R.id.faviconBookmark) ImageView favicon; + + @NonNull private final BookmarkListAdapter adapter; - final Context context; + @Nullable private final OnItemLongClickListener onItemLongClickListener; + @Nullable private final OnItemClickListener onItemClickListener; - public BookmarkViewAdapter(Context context, @NonNull List data) { - super(context, R.layout.bookmark_list_item, data); - this.context = context; + BookmarkViewHolder(@NonNull View itemView, + @NonNull BookmarkListAdapter adapter, + @Nullable OnItemLongClickListener onItemLongClickListener, + @Nullable OnItemClickListener onItemClickListener) { + super(itemView); + ButterKnife.bind(this, itemView); + + this.adapter = adapter; + + this.onItemClickListener = onItemClickListener; + this.onItemLongClickListener = onItemLongClickListener; + + itemView.setOnLongClickListener(this); + itemView.setOnClickListener(this); } - @NonNull @Override - public View getView(int position, View convertView, @NonNull ViewGroup parent) { - View row = convertView; - BookmarkViewHolder holder; - - if (row == null) { - LayoutInflater inflater = LayoutInflater.from(context); - row = inflater.inflate(R.layout.bookmark_list_item, parent, false); - - holder = new BookmarkViewHolder(); - holder.txtTitle = (TextView) row.findViewById(R.id.textBookmark); - holder.favicon = (ImageView) row.findViewById(R.id.faviconBookmark); - row.setTag(holder); - } else { - holder = (BookmarkViewHolder) row.getTag(); + public void onClick(View v) { + int index = getAdapterPosition(); + if (onItemClickListener != null && index != RecyclerView.NO_ID) { + onItemClickListener.onItemClick(adapter.itemAt(index)); } + } + + @Override + public boolean onLongClick(View v) { + int index = getAdapterPosition(); + return index != RecyclerView.NO_POSITION && onItemLongClickListener != null && + onItemLongClickListener.onItemLongClick(adapter.itemAt(index)); + } + } + + interface OnItemLongClickListener { + boolean onItemLongClick(@NonNull HistoryItem item); + } + + interface OnItemClickListener { + void onItemClick(@NonNull HistoryItem item); + } + + private static class BookmarkListAdapter extends RecyclerView.Adapter { - ViewCompat.jumpDrawablesToCurrentState(row); + @NonNull private List mBookmarks = new ArrayList<>(); + @NonNull private final FaviconModel mFaviconModel; + @NonNull private final Bitmap mFolderBitmap; + @NonNull private final Bitmap mWebpageBitmap; + @NonNull private final Map mFaviconFetchSubscriptions = new ConcurrentHashMap<>(); - HistoryItem web = mBookmarks.get(position); + @Nullable private OnItemLongClickListener mOnItemLongCLickListener; + @Nullable private OnItemClickListener mOnItemClickListener; + + BookmarkListAdapter(@NonNull FaviconModel faviconModel, + @NonNull Bitmap folderBitmap, + @NonNull Bitmap webpageBitmap) { + mFaviconModel = faviconModel; + mFolderBitmap = folderBitmap; + mWebpageBitmap = webpageBitmap; + } + + void setOnItemLongClickListener(@Nullable OnItemLongClickListener listener) { + mOnItemLongCLickListener = listener; + } + + void setOnItemClickListener(@Nullable OnItemClickListener listener) { + mOnItemClickListener = listener; + } + + @NonNull + HistoryItem itemAt(int position) { + return mBookmarks.get(position); + } + + void deleteItem(@NonNull HistoryItem item) { + List newList = new ArrayList<>(mBookmarks); + newList.remove(item); + updateItems(newList); + } + + void updateItems(@NonNull List newList) { + final List oldList = mBookmarks; + mBookmarks = newList; + + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() { + @Override + public int getOldListSize() { + return oldList.size(); + } + + @Override + public int getNewListSize() { + return mBookmarks.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return oldList.get(oldItemPosition).equals(mBookmarks.get(newItemPosition)); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return oldList.get(oldItemPosition).equals(mBookmarks.get(newItemPosition)); + } + }); + + diffResult.dispatchUpdatesTo(this); + } + + void cleanupSubscriptions() { + for (Subscription subscription : mFaviconFetchSubscriptions.values()) { + subscription.unsubscribe(); + } + mFaviconFetchSubscriptions.clear(); + } + + @Override + public BookmarkViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + View itemView = inflater.inflate(R.layout.bookmark_list_item, parent, false); + + return new BookmarkViewHolder(itemView, this, mOnItemLongCLickListener, mOnItemClickListener); + } + + @Override + public void onViewRecycled(BookmarkViewHolder holder) { + super.onViewRecycled(holder); + } + + @Override + public void onBindViewHolder(final BookmarkViewHolder holder, int position) { + ViewCompat.jumpDrawablesToCurrentState(holder.itemView); + + final HistoryItem web = mBookmarks.get(position); holder.txtTitle.setText(web.getTitle()); if (web.isFolder()) { holder.favicon.setImageBitmap(mFolderBitmap); } else if (web.getBitmap() == null) { holder.favicon.setImageBitmap(mWebpageBitmap); - new ImageDownloadTask(holder.favicon, web, mWebpageBitmap, BrowserApp.get(context)) - .executeOnExecutor(AsyncExecutor.getInstance()); + holder.favicon.setTag(web.getUrl().hashCode()); + + final String url = web.getUrl(); + + Subscription oldSubscription = mFaviconFetchSubscriptions.get(url); + SubscriptionUtils.safeUnsubscribe(oldSubscription); + + final Subscription faviconSubscription = mFaviconModel.faviconForUrl(url, mWebpageBitmap, true) + .subscribeOn(Schedulers.worker()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable Bitmap item) { + mFaviconFetchSubscriptions.remove(url); + Object tag = holder.favicon.getTag(); + if (tag != null && tag.equals(url.hashCode())) { + holder.favicon.setImageBitmap(item); + } + + web.setBitmap(item); + } + }); + + mFaviconFetchSubscriptions.put(url, faviconSubscription); } else { holder.favicon.setImageBitmap(web.getBitmap()); } - return row; + } - private class BookmarkViewHolder { - TextView txtTitle; - ImageView favicon; + @Override + public int getItemCount() { + return mBookmarks.size(); } } diff --git a/app/src/main/java/acr/browser/lightning/fragment/DisplaySettingsFragment.java b/app/src/main/java/acr/browser/lightning/fragment/DisplaySettingsFragment.java index a76b7b5b5..d5d796c36 100644 --- a/app/src/main/java/acr/browser/lightning/fragment/DisplaySettingsFragment.java +++ b/app/src/main/java/acr/browser/lightning/fragment/DisplaySettingsFragment.java @@ -33,6 +33,7 @@ public class DisplaySettingsFragment extends LightningPreferenceFragment impleme private static final String SETTINGS_TEXTSIZE = "text_size"; private static final String SETTINGS_DRAWERTABS = "cb_drawertabs"; private static final String SETTINGS_SWAPTABS = "cb_swapdrawers"; + private static final String SETTINGS_BLACK_STATUS = "black_status_bar"; private static final float XXLARGE = 30.0f; private static final float XLARGE = 26.0f; @@ -42,8 +43,7 @@ public class DisplaySettingsFragment extends LightningPreferenceFragment impleme private static final float XSMALL = 10.0f; private Activity mActivity; - private CheckBoxPreference cbstatus, cbfullscreen, cbviewport, cboverview, cbreflow; - private Preference theme; + private Preference mTheme; private String[] mThemeOptions; private int mCurrentTheme; @@ -63,35 +63,38 @@ private void initPrefs() { mThemeOptions = this.getResources().getStringArray(R.array.themes); mCurrentTheme = mPreferenceManager.getUseTheme(); - theme = findPreference(SETTINGS_THEME); - Preference textsize = findPreference(SETTINGS_TEXTSIZE); - cbstatus = (CheckBoxPreference) findPreference(SETTINGS_HIDESTATUSBAR); - cbfullscreen = (CheckBoxPreference) findPreference(SETTINGS_FULLSCREEN); - cbviewport = (CheckBoxPreference) findPreference(SETTINGS_VIEWPORT); - cboverview = (CheckBoxPreference) findPreference(SETTINGS_OVERVIEWMODE); - cbreflow = (CheckBoxPreference) findPreference(SETTINGS_REFLOW); + mTheme = findPreference(SETTINGS_THEME); + Preference textSize = findPreference(SETTINGS_TEXTSIZE); + CheckBoxPreference cbStatus = (CheckBoxPreference) findPreference(SETTINGS_HIDESTATUSBAR); + CheckBoxPreference cbFullScreen = (CheckBoxPreference) findPreference(SETTINGS_FULLSCREEN); + CheckBoxPreference cbViewPort = (CheckBoxPreference) findPreference(SETTINGS_VIEWPORT); + CheckBoxPreference cbOverview = (CheckBoxPreference) findPreference(SETTINGS_OVERVIEWMODE); + CheckBoxPreference cbReflow = (CheckBoxPreference) findPreference(SETTINGS_REFLOW); + CheckBoxPreference cbBlackStatus = (CheckBoxPreference) findPreference(SETTINGS_BLACK_STATUS); CheckBoxPreference cbDrawerTabs = (CheckBoxPreference) findPreference(SETTINGS_DRAWERTABS); CheckBoxPreference cbSwapTabs = (CheckBoxPreference) findPreference(SETTINGS_SWAPTABS); - theme.setOnPreferenceClickListener(this); - textsize.setOnPreferenceClickListener(this); - cbstatus.setOnPreferenceChangeListener(this); - cbfullscreen.setOnPreferenceChangeListener(this); - cbviewport.setOnPreferenceChangeListener(this); - cboverview.setOnPreferenceChangeListener(this); - cbreflow.setOnPreferenceChangeListener(this); + mTheme.setOnPreferenceClickListener(this); + textSize.setOnPreferenceClickListener(this); + cbStatus.setOnPreferenceChangeListener(this); + cbFullScreen.setOnPreferenceChangeListener(this); + cbViewPort.setOnPreferenceChangeListener(this); + cbOverview.setOnPreferenceChangeListener(this); + cbReflow.setOnPreferenceChangeListener(this); + cbBlackStatus.setOnPreferenceChangeListener(this); cbDrawerTabs.setOnPreferenceChangeListener(this); cbSwapTabs.setOnPreferenceChangeListener(this); - cbstatus.setChecked(mPreferenceManager.getHideStatusBarEnabled()); - cbfullscreen.setChecked(mPreferenceManager.getFullScreenEnabled()); - cbviewport.setChecked(mPreferenceManager.getUseWideViewportEnabled()); - cboverview.setChecked(mPreferenceManager.getOverviewModeEnabled()); - cbreflow.setChecked(mPreferenceManager.getTextReflowEnabled()); + cbStatus.setChecked(mPreferenceManager.getHideStatusBarEnabled()); + cbFullScreen.setChecked(mPreferenceManager.getFullScreenEnabled()); + cbViewPort.setChecked(mPreferenceManager.getUseWideViewportEnabled()); + cbOverview.setChecked(mPreferenceManager.getOverviewModeEnabled()); + cbReflow.setChecked(mPreferenceManager.getTextReflowEnabled()); + cbBlackStatus.setChecked(mPreferenceManager.getUseBlackStatusBar()); cbDrawerTabs.setChecked(mPreferenceManager.getShowTabsInDrawer(true)); cbSwapTabs.setChecked(mPreferenceManager.getBookmarksAndTabsSwapped()); - theme.setSummary(mThemeOptions[mPreferenceManager.getUseTheme()]); + mTheme.setSummary(mThemeOptions[mPreferenceManager.getUseTheme()]); } @Override @@ -118,23 +121,18 @@ public boolean onPreferenceChange(@NonNull Preference preference, Object newValu switch (preference.getKey()) { case SETTINGS_HIDESTATUSBAR: mPreferenceManager.setHideStatusBarEnabled(checked); - cbstatus.setChecked(checked); return true; case SETTINGS_FULLSCREEN: mPreferenceManager.setFullScreenEnabled(checked); - cbfullscreen.setChecked(checked); return true; case SETTINGS_VIEWPORT: mPreferenceManager.setUseWideViewportEnabled(checked); - cbviewport.setChecked(checked); return true; case SETTINGS_OVERVIEWMODE: mPreferenceManager.setOverviewModeEnabled(checked); - cboverview.setChecked(checked); return true; case SETTINGS_REFLOW: mPreferenceManager.setTextReflowEnabled(checked); - cbreflow.setChecked(checked); return true; case SETTINGS_DRAWERTABS: mPreferenceManager.setShowTabsInDrawer(checked); @@ -142,6 +140,9 @@ public boolean onPreferenceChange(@NonNull Preference preference, Object newValu case SETTINGS_SWAPTABS: mPreferenceManager.setBookmarkAndTabsSwapped(checked); return true; + case SETTINGS_BLACK_STATUS: + mPreferenceManager.setUseBlackStatusBar(checked); + return true; default: return false; } @@ -150,7 +151,7 @@ public boolean onPreferenceChange(@NonNull Preference preference, Object newValu private void textSizePicker() { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); LayoutInflater inflater = getActivity().getLayoutInflater(); - LinearLayout view = (LinearLayout) inflater.inflate(R.layout.seek_layout, null); + LinearLayout view = (LinearLayout) inflater.inflate(R.layout.dialog_seek_bar, null); final SeekBar bar = (SeekBar) view.findViewById(R.id.text_size_seekbar); final TextView sample = new TextView(getActivity()); sample.setText(R.string.untitled); @@ -205,7 +206,7 @@ private void themePicker() { public void onClick(DialogInterface dialog, int which) { mPreferenceManager.setUseTheme(which); if (which < mThemeOptions.length) { - theme.setSummary(mThemeOptions[which]); + mTheme.setSummary(mThemeOptions[which]); } } }); @@ -233,13 +234,13 @@ public void onCancel(DialogInterface dialog) { private static class TextSeekBarListener implements SeekBar.OnSeekBarChangeListener { - private final TextView sample; + private final TextView mSample; - public TextSeekBarListener(TextView sample) {this.sample = sample;} + public TextSeekBarListener(TextView sample) {this.mSample = sample;} @Override public void onProgressChanged(SeekBar view, int size, boolean user) { - this.sample.setTextSize(getTextSize(size)); + this.mSample.setTextSize(getTextSize(size)); } @Override diff --git a/app/src/main/java/acr/browser/lightning/fragment/GeneralSettingsFragment.java b/app/src/main/java/acr/browser/lightning/fragment/GeneralSettingsFragment.java index 1ae471b84..d22d11271 100644 --- a/app/src/main/java/acr/browser/lightning/fragment/GeneralSettingsFragment.java +++ b/app/src/main/java/acr/browser/lightning/fragment/GeneralSettingsFragment.java @@ -17,11 +17,11 @@ import android.text.Editable; import android.text.InputFilter; import android.text.TextWatcher; +import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.widget.EditText; -import android.widget.LinearLayout; +import acr.browser.lightning.BuildConfig; import acr.browser.lightning.R; import acr.browser.lightning.constant.Constants; import acr.browser.lightning.dialog.BrowserDialog; @@ -30,7 +30,7 @@ import acr.browser.lightning.utils.ThemeUtils; import acr.browser.lightning.utils.Utils; -import static acr.browser.lightning.preference.PreferenceManager.*; +import static acr.browser.lightning.preference.PreferenceManager.Suggestion; public class GeneralSettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener { @@ -117,6 +117,9 @@ private void initPrefs() { case SUGGESTION_DUCK: searchsSuggestions.setSummary(R.string.powered_by_duck); break; + case SUGGESTION_BAIDU: + searchsSuggestions.setSummary(R.string.powered_by_baidu); + break; case SUGGESTION_NONE: searchsSuggestions.setSummary(R.string.search_suggestions_off); break; @@ -151,35 +154,37 @@ private void initPrefs() { boolean imagesBool = mPreferenceManager.getBlockImagesEnabled(); boolean enableJSBool = mPreferenceManager.getJavaScriptEnabled(); - cbAds.setEnabled(Constants.FULL_VERSION); - cbFlash.setEnabled(API < Build.VERSION_CODES.KITKAT); + cbAds.setEnabled(BuildConfig.FULL_VERSION); + + if (API < Build.VERSION_CODES.KITKAT) { + cbFlash.setEnabled(true); + } else { + cbFlash.setEnabled(false); + cbFlash.setSummary(getResources().getString(R.string.flash_not_supported)); + } cbImages.setChecked(imagesBool); cbJsScript.setChecked(enableJSBool); cbFlash.setChecked(flashNum > 0); - cbAds.setChecked(Constants.FULL_VERSION && mPreferenceManager.getAdBlockEnabled()); + cbAds.setChecked(BuildConfig.FULL_VERSION && mPreferenceManager.getAdBlockEnabled()); cbColorMode.setChecked(mPreferenceManager.getColorModeEnabled()); } private void searchUrlPicker() { - final AlertDialog.Builder urlPicker = new AlertDialog.Builder(mActivity); - urlPicker.setTitle(getResources().getString(R.string.custom_url)); - final EditText getSearchUrl = new EditText(mActivity); - String mSearchUrl = mPreferenceManager.getSearchUrl(); - getSearchUrl.setText(mSearchUrl); - urlPicker.setView(getSearchUrl); - urlPicker.setPositiveButton(getResources().getString(R.string.action_ok), - new DialogInterface.OnClickListener() { + + BrowserDialog.showEditText(mActivity, + R.string.custom_url, + R.string.custom_url, + mPreferenceManager.getSearchUrl(), + R.string.action_ok, + new BrowserDialog.EditorListener() { @Override - public void onClick(DialogInterface dialog, int which) { - String text = getSearchUrl.getText().toString(); + public void onClick(String text) { mPreferenceManager.setSearchUrl(text); - searchengine.setSummary(getResources().getString(R.string.custom_url) + ": " - + text); + searchengine.setSummary(mActivity.getString(R.string.custom_url) + ": " + text); } }); - Dialog dialog = urlPicker.show(); - BrowserDialog.setDialogSize(mActivity, dialog); + } private void getFlashChoice() { @@ -216,7 +221,7 @@ public void onCancel(DialogInterface dialog) { private void proxyChoicePicker() { AlertDialog.Builder picker = new AlertDialog.Builder(mActivity); - picker.setTitle(getResources().getString(R.string.http_proxy)); + picker.setTitle(R.string.http_proxy); picker.setSingleChoiceItems(mProxyChoices, mPreferenceManager.getProxyChoice(), new DialogInterface.OnClickListener() { @@ -225,12 +230,12 @@ public void onClick(DialogInterface dialog, int which) { setProxyChoice(which); } }); - picker.setPositiveButton(getResources().getString(R.string.action_ok), null); + picker.setPositiveButton(R.string.action_ok, null); Dialog dialog = picker.show(); BrowserDialog.setDialogSize(mActivity, dialog); } - private void setProxyChoice(@Constants.PROXY int choice) { + private void setProxyChoice(@Constants.Proxy int choice) { switch (choice) { case Constants.PROXY_ORBOT: choice = ProxyUtils.setProxyChoice(choice, mActivity); @@ -251,7 +256,7 @@ private void setProxyChoice(@Constants.PROXY int choice) { } private void manualProxyPicker() { - View v = mActivity.getLayoutInflater().inflate(R.layout.picker_manual_proxy, null); + View v = mActivity.getLayoutInflater().inflate(R.layout.dialog_manual_proxy, null); final EditText eProxyHost = (EditText) v.findViewById(R.id.proxyHost); final EditText eProxyPort = (EditText) v.findViewById(R.id.proxyPort); @@ -307,14 +312,14 @@ public void onClick(DialogInterface dialog, int which) { setSearchEngineSummary(which); } }); - picker.setPositiveButton(getResources().getString(R.string.action_ok), null); + picker.setPositiveButton(R.string.action_ok, null); Dialog dialog = picker.show(); BrowserDialog.setDialogSize(mActivity, dialog); } private void homepageDialog() { AlertDialog.Builder picker = new AlertDialog.Builder(mActivity); - picker.setTitle(getResources().getString(R.string.home)); + picker.setTitle(R.string.home); mHomepage = mPreferenceManager.getHomepage(); int n; switch (mHomepage) { @@ -336,20 +341,20 @@ private void homepageDialog() { new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - switch (which + 1) { - case 1: + switch (which) { + case 0: mPreferenceManager.setHomepage(Constants.SCHEME_HOMEPAGE); home.setSummary(getResources().getString(R.string.action_homepage)); break; - case 2: + case 1: mPreferenceManager.setHomepage(Constants.SCHEME_BLANK); home.setSummary(getResources().getString(R.string.action_blank)); break; - case 3: + case 2: mPreferenceManager.setHomepage(Constants.SCHEME_BOOKMARKS); home.setSummary(getResources().getString(R.string.action_bookmarks)); break; - case 4: + case 3: homePicker(); break; } @@ -364,7 +369,7 @@ private void suggestionsDialog() { AlertDialog.Builder picker = new AlertDialog.Builder(mActivity); picker.setTitle(getResources().getString(R.string.search_suggestions)); - int currentChoice = 2; + int currentChoice = 3; switch (mPreferenceManager.getSearchSuggestionChoice()) { case SUGGESTION_GOOGLE: @@ -373,9 +378,12 @@ private void suggestionsDialog() { case SUGGESTION_DUCK: currentChoice = 1; break; - case SUGGESTION_NONE: + case SUGGESTION_BAIDU: currentChoice = 2; break; + case SUGGESTION_NONE: + currentChoice = 3; + break; } picker.setSingleChoiceItems(R.array.suggestions, currentChoice, @@ -392,6 +400,10 @@ public void onClick(DialogInterface dialog, int which) { searchsSuggestions.setSummary(R.string.powered_by_duck); break; case 2: + mPreferenceManager.setSearchSuggestionChoice(Suggestion.SUGGESTION_BAIDU); + searchsSuggestions.setSummary(R.string.powered_by_baidu); + break; + case 3: mPreferenceManager.setSearchSuggestionChoice(Suggestion.SUGGESTION_NONE); searchsSuggestions.setSummary(R.string.search_suggestions_off); break; @@ -404,28 +416,26 @@ public void onClick(DialogInterface dialog, int which) { } private void homePicker() { - final AlertDialog.Builder homePicker = new AlertDialog.Builder(mActivity); - homePicker.setTitle(getResources().getString(R.string.title_custom_homepage)); - final EditText getHome = new EditText(mActivity); + String currentHomepage; mHomepage = mPreferenceManager.getHomepage(); if (!mHomepage.startsWith(Constants.ABOUT)) { - getHome.setText(mHomepage); + currentHomepage = mHomepage; } else { - String defaultUrl = "https://www.google.com"; - getHome.setText(defaultUrl); + currentHomepage = "https://www.google.com"; } - homePicker.setView(getHome); - homePicker.setPositiveButton(getResources().getString(R.string.action_ok), - new DialogInterface.OnClickListener() { + + BrowserDialog.showEditText(mActivity, + R.string.title_custom_homepage, + R.string.title_custom_homepage, + currentHomepage, + R.string.action_ok, + new BrowserDialog.EditorListener() { @Override - public void onClick(DialogInterface dialog, int which) { - String text = getHome.getText().toString(); + public void onClick(String text) { mPreferenceManager.setHomepage(text); home.setSummary(text); } }); - Dialog dialog = homePicker.show(); - BrowserDialog.setDialogSize(mActivity, dialog); } private void downloadLocDialog() { @@ -468,17 +478,17 @@ private void agentDialog() { @Override public void onClick(DialogInterface dialog, int which) { mPreferenceManager.setUserAgentChoice(which + 1); - switch (which + 1) { - case 1: + switch (which) { + case 0: useragent.setSummary(getResources().getString(R.string.agent_default)); break; - case 2: + case 1: useragent.setSummary(getResources().getString(R.string.agent_desktop)); break; - case 3: + case 2: useragent.setSummary(getResources().getString(R.string.agent_mobile)); break; - case 4: + case 3: useragent.setSummary(getResources().getString(R.string.agent_custom)); agentPicker(); break; @@ -491,49 +501,45 @@ public void onClick(DialogInterface dialog, int which) { } private void agentPicker() { - final AlertDialog.Builder agentStringPicker = new AlertDialog.Builder(mActivity); - agentStringPicker.setTitle(getResources().getString(R.string.title_user_agent)); - final EditText getAgent = new EditText(mActivity); - agentStringPicker.setView(getAgent); - agentStringPicker.setPositiveButton(getResources().getString(R.string.action_ok), - new DialogInterface.OnClickListener() { + + BrowserDialog.showEditText(mActivity, + R.string.title_user_agent, + R.string.title_user_agent, + mPreferenceManager.getUserAgentString(""), + R.string.action_ok, + new BrowserDialog.EditorListener() { @Override - public void onClick(DialogInterface dialog, int which) { - String text = getAgent.getText().toString(); + public void onClick(String text) { mPreferenceManager.setUserAgentString(text); - useragent.setSummary(getResources().getString(R.string.agent_custom)); + useragent.setSummary(mActivity.getString(R.string.agent_custom)); } }); - Dialog dialog = agentStringPicker.show(); - BrowserDialog.setDialogSize(mActivity, dialog); } private void downPicker() { - final AlertDialog.Builder downLocationPicker = new AlertDialog.Builder(mActivity); - LinearLayout layout = new LinearLayout(mActivity); - downLocationPicker.setTitle(getResources().getString(R.string.title_download_location)); - final EditText getDownload = new EditText(mActivity); - getDownload.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - getDownload.setText(mPreferenceManager.getDownloadDirectory()); - final int errorColor = ContextCompat.getColor(getActivity(), R.color.error_red); - final int regularColor = ThemeUtils.getTextColor(getActivity()); + + View dialogView = LayoutInflater.from(mActivity).inflate(R.layout.dialog_edit_text, null); + final EditText getDownload = (EditText) dialogView.findViewById(R.id.dialog_edit_text); + + final int errorColor = ContextCompat.getColor(mActivity, R.color.error_red); + final int regularColor = ThemeUtils.getTextColor(mActivity); getDownload.setTextColor(regularColor); getDownload.addTextChangedListener(new DownloadLocationTextWatcher(getDownload, errorColor, regularColor)); getDownload.setText(mPreferenceManager.getDownloadDirectory()); - layout.addView(getDownload); - downLocationPicker.setView(layout); - downLocationPicker.setPositiveButton(getResources().getString(R.string.action_ok), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - String text = getDownload.getText().toString(); - text = DownloadHandler.addNecessarySlashes(text); - mPreferenceManager.setDownloadDirectory(text); - downloadloc.setSummary(text); - } - }); + AlertDialog.Builder downLocationPicker = new AlertDialog.Builder(mActivity) + .setTitle(R.string.title_download_location) + .setView(dialogView) + .setPositiveButton(R.string.action_ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String text = getDownload.getText().toString(); + text = DownloadHandler.addNecessarySlashes(text); + mPreferenceManager.setDownloadDirectory(text); + downloadloc.setSummary(text); + } + }); Dialog dialog = downLocationPicker.show(); BrowserDialog.setDialogSize(mActivity, dialog); } @@ -639,11 +645,11 @@ public boolean onPreferenceChange(@NonNull Preference preference, Object newValu } private static class DownloadLocationTextWatcher implements TextWatcher { - private final EditText getDownload; + @NonNull private final EditText getDownload; private final int errorColor; private final int regularColor; - public DownloadLocationTextWatcher(EditText getDownload, int errorColor, int regularColor) { + public DownloadLocationTextWatcher(@NonNull EditText getDownload, int errorColor, int regularColor) { this.getDownload = getDownload; this.errorColor = errorColor; this.regularColor = regularColor; diff --git a/app/src/main/java/acr/browser/lightning/fragment/PrivacySettingsFragment.java b/app/src/main/java/acr/browser/lightning/fragment/PrivacySettingsFragment.java index 5d1ca65bf..f3d6ecc40 100644 --- a/app/src/main/java/acr/browser/lightning/fragment/PrivacySettingsFragment.java +++ b/app/src/main/java/acr/browser/lightning/fragment/PrivacySettingsFragment.java @@ -8,19 +8,20 @@ import android.content.DialogInterface; import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.Message; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.support.annotation.NonNull; import android.support.v7.app.AlertDialog; import android.webkit.WebView; -import javax.inject.Inject; +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableAction; +import com.anthonycr.bonsai.CompletableOnSubscribe; +import com.anthonycr.bonsai.CompletableSubscriber; +import com.anthonycr.bonsai.Schedulers; import acr.browser.lightning.R; import acr.browser.lightning.app.BrowserApp; -import acr.browser.lightning.database.HistoryDatabase; import acr.browser.lightning.dialog.BrowserDialog; import acr.browser.lightning.utils.Utils; import acr.browser.lightning.utils.WebUtils; @@ -43,9 +44,6 @@ public class PrivacySettingsFragment extends LightningPreferenceFragment impleme private static final String SETTINGS_IDENTIFYINGHEADERS = "remove_identifying_headers"; private Activity mActivity; - private Handler mMessageHandler; - - @Inject HistoryDatabase mHistoryDatabase; @Override public void onCreate(Bundle savedInstanceState) { @@ -108,29 +106,6 @@ private void initPrefs() { cb3cookies.setEnabled(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP); - mMessageHandler = new MessageHandler(mActivity); - } - - private static class MessageHandler extends Handler { - - final Activity mHandlerContext; - - public MessageHandler(Activity context) { - this.mHandlerContext = context; - } - - @Override - public void handleMessage(@NonNull Message msg) { - switch (msg.what) { - case 1: - Utils.showSnackbar(mHandlerContext, R.string.message_clear_history); - break; - case 2: - Utils.showSnackbar(mHandlerContext, R.string.message_cookies_cleared); - break; - } - super.handleMessage(msg); - } } @Override @@ -157,19 +132,22 @@ private void clearHistoryDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); builder.setTitle(getResources().getString(R.string.title_clear_history)); Dialog dialog = builder.setMessage(getResources().getString(R.string.dialog_history)) - .setPositiveButton(getResources().getString(R.string.action_yes), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface arg0, int arg1) { - BrowserApp.getIOThread().execute(new Runnable() { - @Override - public void run() { - clearHistory(); - } - }); - } - }) - .setNegativeButton(getResources().getString(R.string.action_no), null).show(); + .setPositiveButton(getResources().getString(R.string.action_yes), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface arg0, int arg1) { + clearHistory() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onComplete() { + Utils.showSnackbar(getActivity(), R.string.message_clear_history); + } + }); + } + }) + .setNegativeButton(getResources().getString(R.string.action_no), null).show(); BrowserDialog.setDialogSize(mActivity, dialog); } @@ -177,19 +155,22 @@ private void clearCookiesDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); builder.setTitle(getResources().getString(R.string.title_clear_cookies)); builder.setMessage(getResources().getString(R.string.dialog_cookies)) - .setPositiveButton(getResources().getString(R.string.action_yes), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface arg0, int arg1) { - BrowserApp.getTaskThread().execute(new Runnable() { - @Override - public void run() { - clearCookies(); - } - }); - } - }) - .setNegativeButton(getResources().getString(R.string.action_no), null).show(); + .setPositiveButton(getResources().getString(R.string.action_yes), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface arg0, int arg1) { + clearCookies() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new CompletableOnSubscribe() { + @Override + public void onComplete() { + Utils.showSnackbar(getActivity(), R.string.message_cookies_cleared); + } + }); + } + }) + .setNegativeButton(getResources().getString(R.string.action_no), null).show(); } private void clearCache() { @@ -199,14 +180,34 @@ private void clearCache() { Utils.showSnackbar(mActivity, R.string.message_cache_cleared); } - private void clearHistory() { - WebUtils.clearHistory(getActivity(), mHistoryDatabase); - mMessageHandler.sendEmptyMessage(1); + @NonNull + private Completable clearHistory() { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + Activity activity = getActivity(); + if (activity != null) { + WebUtils.clearHistory(activity); + subscriber.onComplete(); + } + subscriber.onError(new RuntimeException("Activity was null in clearHistory")); + } + }); } - private void clearCookies() { - WebUtils.clearCookies(getActivity()); - mMessageHandler.sendEmptyMessage(2); + @NonNull + private Completable clearCookies() { + return Completable.create(new CompletableAction() { + @Override + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + Activity activity = getActivity(); + if (activity != null) { + WebUtils.clearCookies(activity); + subscriber.onComplete(); + } + subscriber.onError(new RuntimeException("Activity was null in clearCookies")); + } + }); } private void clearWebStorage() { diff --git a/app/src/main/java/acr/browser/lightning/fragment/TabsFragment.java b/app/src/main/java/acr/browser/lightning/fragment/TabsFragment.java index fdb9799dc..62642cfe2 100644 --- a/app/src/main/java/acr/browser/lightning/fragment/TabsFragment.java +++ b/app/src/main/java/acr/browser/lightning/fragment/TabsFragment.java @@ -31,8 +31,6 @@ import android.widget.LinearLayout; import android.widget.TextView; -import com.squareup.otto.Bus; - import javax.inject.Inject; import acr.browser.lightning.R; @@ -48,6 +46,9 @@ import acr.browser.lightning.utils.Utils; import acr.browser.lightning.view.BackgroundDrawable; import acr.browser.lightning.view.LightningView; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.Unbinder; /** * A fragment that holds and manages the tabs and interaction with the tabs. @@ -58,14 +59,25 @@ */ public class TabsFragment extends Fragment implements View.OnClickListener, View.OnLongClickListener, TabsView { - private static final String TAG = TabsFragment.class.getSimpleName(); + @NonNull + public static TabsFragment createTabsFragment(boolean isIncognito, boolean showTabsInDrawer) { + TabsFragment tabsFragment = new TabsFragment(); + final Bundle tabsFragmentArguments = new Bundle(); + tabsFragmentArguments.putBoolean(TabsFragment.IS_INCOGNITO, isIncognito); + tabsFragmentArguments.putBoolean(TabsFragment.VERTICAL_MODE, showTabsInDrawer); + tabsFragment.setArguments(tabsFragmentArguments); + + return tabsFragment; + } + + private static final String TAG = "TabsFragment"; /** * Arguments boolean to tell the fragment it is displayed in the drawner or on the tab strip * If true, the fragment is in the left drawner in the strip otherwise. */ - public static final String VERTICAL_MODE = TAG + ".VERTICAL_MODE"; - public static final String IS_INCOGNITO = TAG + ".IS_INCOGNITO"; + private static final String VERTICAL_MODE = TAG + ".VERTICAL_MODE"; + private static final String IS_INCOGNITO = TAG + ".IS_INCOGNITO"; private boolean mIsIncognito, mDarkTheme; private int mIconColor; @@ -74,10 +86,11 @@ public class TabsFragment extends Fragment implements View.OnClickListener, View @Nullable private LightningViewAdapter mTabsAdapter; private UIController mUiController; - private RecyclerView mRecyclerView; + + @BindView(R.id.tabs_list) RecyclerView mRecyclerView; + private Unbinder mUnbinder; private TabsManager mTabsManager; - @Inject Bus mBus; @Inject PreferenceManager mPreferences; public TabsFragment() { @@ -126,7 +139,9 @@ public void onClick(View v) { } }); } - mRecyclerView = (RecyclerView) view.findViewById(R.id.tabs_list); + + mUnbinder = ButterKnife.bind(this, view); + SimpleItemAnimator animator; if (mShowInNavigationDrawer) { animator = new VerticalItemAnimator(); @@ -144,9 +159,20 @@ public void onClick(View v) { mTabsAdapter = new LightningViewAdapter(mShowInNavigationDrawer); mRecyclerView.setAdapter(mTabsAdapter); mRecyclerView.setHasFixedSize(true); + return view; } + @Override + public void onDestroyView() { + super.onDestroyView(); + if (mUnbinder != null) { + mUnbinder.unbind(); + mUnbinder = null; + } + mTabsAdapter = null; + } + private TabsManager getTabsManager() { if (mTabsManager == null) { mTabsManager = mUiController.getTabModel(); @@ -163,18 +189,6 @@ private void setupFrameLayoutButton(@NonNull final View root, @IdRes final int b buttonImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN); } - @Override - public void onDestroyView() { - super.onDestroyView(); - mTabsAdapter = null; - } - - @Override - public void onStart() { - super.onStart(); - mBus.register(this); - } - @Override public void onResume() { super.onResume(); @@ -184,12 +198,6 @@ public void onResume() { } } - @Override - public void onStop() { - super.onStop(); - mBus.unregister(this); - } - @Override public void tabsInitialized() { if (mTabsAdapter != null) { diff --git a/app/src/main/java/acr/browser/lightning/fragment/anim/HorizontalItemAnimator.java b/app/src/main/java/acr/browser/lightning/fragment/anim/HorizontalItemAnimator.java index 197d36445..732cf9652 100644 --- a/app/src/main/java/acr/browser/lightning/fragment/anim/HorizontalItemAnimator.java +++ b/app/src/main/java/acr/browser/lightning/fragment/anim/HorizontalItemAnimator.java @@ -15,7 +15,8 @@ */ package acr.browser.lightning.fragment.anim; -import android.support.v4.animation.AnimatorCompatHelper; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPropertyAnimatorCompat; import android.support.v4.view.ViewPropertyAnimatorListener; @@ -54,6 +55,8 @@ public class HorizontalItemAnimator extends SimpleItemAnimator { private final ArrayList mRemoveAnimations = new ArrayList<>(); private final ArrayList mChangeAnimations = new ArrayList<>(); + private TimeInterpolator mDefaultInterpolator; + private static class MoveInfo { public final ViewHolder holder; public final int fromX; @@ -544,10 +547,17 @@ public void endAnimation(ViewHolder item) { } private void resetAnimation(ViewHolder holder) { - AnimatorCompatHelper.clearInterpolator(holder.itemView); + clearInterpolator(holder.itemView); endAnimation(holder); } + private void clearInterpolator(View view) { + if (mDefaultInterpolator == null) { + mDefaultInterpolator = new ValueAnimator().getInterpolator(); + } + view.animate().setInterpolator(mDefaultInterpolator); + } + @Override public boolean isRunning() { return (!mPendingAdditions.isEmpty() || diff --git a/app/src/main/java/acr/browser/lightning/fragment/anim/VerticalItemAnimator.java b/app/src/main/java/acr/browser/lightning/fragment/anim/VerticalItemAnimator.java index 228637ad6..4a2f5eedb 100644 --- a/app/src/main/java/acr/browser/lightning/fragment/anim/VerticalItemAnimator.java +++ b/app/src/main/java/acr/browser/lightning/fragment/anim/VerticalItemAnimator.java @@ -15,7 +15,8 @@ */ package acr.browser.lightning.fragment.anim; -import android.support.v4.animation.AnimatorCompatHelper; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPropertyAnimatorCompat; import android.support.v4.view.ViewPropertyAnimatorListener; @@ -54,6 +55,8 @@ public class VerticalItemAnimator extends SimpleItemAnimator { private final ArrayList mRemoveAnimations = new ArrayList<>(); private final ArrayList mChangeAnimations = new ArrayList<>(); + private TimeInterpolator mDefaultInterpolator; + private static class MoveInfo { public final ViewHolder holder; public final int fromX; @@ -543,10 +546,17 @@ public void endAnimation(ViewHolder item) { } private void resetAnimation(ViewHolder holder) { - AnimatorCompatHelper.clearInterpolator(holder.itemView); + clearInterpolator(holder.itemView); endAnimation(holder); } + private void clearInterpolator(View view) { + if (mDefaultInterpolator == null) { + mDefaultInterpolator = new ValueAnimator().getInterpolator(); + } + view.animate().setInterpolator(mDefaultInterpolator); + } + @Override public boolean isRunning() { return (!mPendingAdditions.isEmpty() || diff --git a/app/src/main/java/acr/browser/lightning/preference/PreferenceManager.java b/app/src/main/java/acr/browser/lightning/preference/PreferenceManager.java index a1855e99c..8be0ac56b 100644 --- a/app/src/main/java/acr/browser/lightning/preference/PreferenceManager.java +++ b/app/src/main/java/acr/browser/lightning/preference/PreferenceManager.java @@ -55,6 +55,7 @@ private static class Name { static final String IDENTIFYING_HEADERS = "removeIdentifyingHeaders"; static final String SWAP_BOOKMARKS_AND_TABS = "swapBookmarksAndTabs"; static final String SEARCH_SUGGESTIONS = "searchSuggestions"; + static final String BLACK_STATUS_BAR = "blackStatusBar"; static final String USE_PROXY = "useProxy"; static final String PROXY_CHOICE = "proxyChoice"; @@ -69,6 +70,7 @@ private static class Name { public enum Suggestion { SUGGESTION_GOOGLE, SUGGESTION_DUCK, + SUGGESTION_BAIDU, SUGGESTION_NONE } @@ -156,7 +158,7 @@ public int getFlashSupport() { } public boolean getFullScreenEnabled() { - return mPrefs.getBoolean(Name.FULL_SCREEN, false); + return mPrefs.getBoolean(Name.FULL_SCREEN, true); } public boolean getHideStatusBarEnabled() { @@ -251,9 +253,9 @@ public boolean getUseProxy() { return mPrefs.getBoolean(Name.USE_PROXY, false); } - @Constants.PROXY + @Constants.Proxy public int getProxyChoice() { - @Constants.PROXY int proxy = mPrefs.getInt(Name.PROXY_CHOICE, Constants.NO_PROXY); + @Constants.Proxy int proxy = mPrefs.getInt(Name.PROXY_CHOICE, Constants.NO_PROXY); switch (proxy) { case Constants.NO_PROXY: case Constants.PROXY_ORBOT: @@ -295,6 +297,10 @@ public boolean getRemoveIdentifyingHeadersEnabled() { return mPrefs.getBoolean(Name.IDENTIFYING_HEADERS, false); } + public boolean getUseBlackStatusBar() { + return mPrefs.getBoolean(Name.BLACK_STATUS_BAR, false); + } + private void putBoolean(@NonNull String name, boolean value) { mPrefs.edit().putBoolean(name, value).apply(); } @@ -307,6 +313,10 @@ private void putString(@NonNull String name, @Nullable String value) { mPrefs.edit().putString(name, value).apply(); } + public void setUseBlackStatusBar(boolean enabled) { + putBoolean(Name.BLACK_STATUS_BAR, enabled); + } + public void setRemoveIdentifyingHeadersEnabled(boolean enabled) { putBoolean(Name.IDENTIFYING_HEADERS, enabled); } @@ -473,7 +483,7 @@ public boolean getUseLeakCanary() { * * @param choice the proxy to use. */ - public void setProxyChoice(@Constants.PROXY int choice) { + public void setProxyChoice(@Constants.Proxy int choice) { putBoolean(Name.USE_PROXY, choice != Constants.NO_PROXY); putInt(Name.PROXY_CHOICE, choice); } diff --git a/app/src/main/java/acr/browser/lightning/reading/Converter.java b/app/src/main/java/acr/browser/lightning/reading/Converter.java index 6e3ba338c..6f35f8c03 100644 --- a/app/src/main/java/acr/browser/lightning/reading/Converter.java +++ b/app/src/main/java/acr/browser/lightning/reading/Converter.java @@ -25,16 +25,16 @@ import java.nio.charset.Charset; import java.util.Locale; -import acr.browser.lightning.constant.Constants; - /** * This class is not thread safe. Use one new instance every time due to * encoding variable. - * + * * @author Peter Karich */ public class Converter { + private static final String TAG = "Converter"; + private final static String UTF8 = "UTF-8"; private final static String ISO = "ISO-8859-1"; private final static int K2 = 2048; @@ -93,7 +93,7 @@ public String streamToString(InputStream is, String enc) { /** * reads bytes off the string and returns a string - * + * * @param is input stream to read * @param maxBytes * The max bytes that we want to read from the input stream @@ -118,20 +118,20 @@ private String streamToString(InputStream is, int maxBytes, String enc) { if (tmpEnc != null) encoding = tmpEnc; else { - Log.d(Constants.TAG, "no charset found in first stage"); + Log.d(TAG, "no charset found in first stage"); // detect with the help of xml beginning ala // encoding="charset" tmpEnc = detectCharset("encoding=", output, in, encoding); if (tmpEnc != null) encoding = tmpEnc; else - Log.d(Constants.TAG, "no charset found in second stage"); + Log.d(TAG, "no charset found in second stage"); } if (!Charset.isSupported(encoding)) throw new UnsupportedEncodingException(encoding); } catch (UnsupportedEncodingException e) { - Log.d(Constants.TAG, + Log.d(TAG, "Using default encoding:" + UTF8 + " problem:" + e.getMessage() + " encoding:" + encoding + ' ' + url); encoding = UTF8; @@ -146,7 +146,7 @@ private String streamToString(InputStream is, int maxBytes, String enc) { byte[] arr = new byte[K2]; while (true) { if (bytesRead >= maxBytes) { - Log.d(Constants.TAG, "Maxbyte of " + maxBytes + Log.d(TAG, "Maxbyte of " + maxBytes + " exceeded! Maybe html is now broken but try it nevertheless. Url: " + url); break; @@ -161,7 +161,7 @@ private String streamToString(InputStream is, int maxBytes, String enc) { return output.toString(encoding); } catch (IOException e) { - Log.e(Constants.TAG, e.toString() + " url:" + url); + Log.e(TAG, e.toString() + " url:" + url); } finally { if (in != null) { try { @@ -178,7 +178,7 @@ private String streamToString(InputStream is, int maxBytes, String enc) { * This method detects the charset even if the first call only returns some * bytes. It will read until 4K bytes are reached and then try to determine * the encoding - * + * * @throws IOException */ private static String detectCharset(String key, ByteArrayOutputStream bos, BufferedInputStream in, @@ -236,7 +236,7 @@ else if (startChar == '\"') bos.reset(); return tmpEnc; } catch (IOException ex) { - Log.e(Constants.TAG, "Couldn't reset stream to re-read with new encoding " + Log.e(TAG, "Couldn't reset stream to re-read with new encoding " + tmpEnc + ' ' + ex.toString()); } } diff --git a/app/src/main/java/acr/browser/lightning/receiver/NetworkReceiver.java b/app/src/main/java/acr/browser/lightning/receiver/NetworkReceiver.java index a5a382510..29d3bc6b5 100644 --- a/app/src/main/java/acr/browser/lightning/receiver/NetworkReceiver.java +++ b/app/src/main/java/acr/browser/lightning/receiver/NetworkReceiver.java @@ -12,7 +12,7 @@ public abstract class NetworkReceiver extends BroadcastReceiver { public abstract void onConnectivityChange(boolean isConnected); @Override - public void onReceive(Context context, Intent intent) { + public void onReceive(@NonNull Context context, Intent intent) { onConnectivityChange(isConnected(context)); } diff --git a/app/src/main/java/acr/browser/lightning/search/BaiduSuggestionsModel.java b/app/src/main/java/acr/browser/lightning/search/BaiduSuggestionsModel.java new file mode 100644 index 000000000..412bfd8d7 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/search/BaiduSuggestionsModel.java @@ -0,0 +1,53 @@ +package acr.browser.lightning.search; + +import android.app.Application; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.json.JSONArray; +import org.xmlpull.v1.XmlPullParser; + +import java.io.InputStream; +import java.util.List; + +import acr.browser.lightning.R; +import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.utils.FileUtils; + +// http://unionsug.baidu.com/su?wd=encodeURIComponent(U) +// http://suggestion.baidu.com/s?wd=encodeURIComponent(U)&action=opensearch + + +class BaiduSuggestionsModel extends BaseSuggestionsModel { + + @NonNull private static final String ENCODING = "UTF-8"; + @Nullable private static XmlPullParser sXpp; + @NonNull private final String mSearchSubtitle; + + BaiduSuggestionsModel(@NonNull Application application) { + super(application, ENCODING); + mSearchSubtitle = application.getString(R.string.suggestion); + } + + @NonNull + protected String createQueryUrl(@NonNull String query, @NonNull String language) { + return "http://suggestion.baidu.com/s?wd=" + query + "&action=opensearch"; + } + + @Override + protected void parseResults(@NonNull InputStream inputStream, @NonNull List results) throws Exception { + String content = FileUtils.readStringFromStream(inputStream, "GBK"); + JSONArray respArray = new JSONArray(content); + JSONArray jsonArray = respArray.getJSONArray(1); + int counter = 0; + for (int n = 0, size = jsonArray.length(); n < size; n++) { + String suggestion = jsonArray.getString(n); + results.add(new HistoryItem(mSearchSubtitle + " \"" + suggestion + '"', + suggestion, R.drawable.ic_search)); + counter++; + if (counter >= MAX_RESULTS) { + break; + } + } + } +} diff --git a/app/src/main/java/acr/browser/lightning/search/BaseSuggestionsModel.java b/app/src/main/java/acr/browser/lightning/search/BaseSuggestionsModel.java new file mode 100644 index 000000000..7dfcf47e0 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/search/BaseSuggestionsModel.java @@ -0,0 +1,135 @@ +package acr.browser.lightning.search; + +import android.app.Application; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.utils.FileUtils; +import acr.browser.lightning.utils.Utils; +import okhttp3.Cache; +import okhttp3.CacheControl; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +abstract class BaseSuggestionsModel { + + private static final String TAG = "BaseSuggestionsModel"; + + static final int MAX_RESULTS = 5; + private static final long INTERVAL_DAY = TimeUnit.DAYS.toSeconds(1); + @NonNull private static final String DEFAULT_LANGUAGE = "en"; + + @NonNull private final OkHttpClient mHttpClient; + @NonNull private final CacheControl mCacheControl; + @NonNull private final String mEncoding; + @NonNull private final String mLanguage; + + @NonNull + protected abstract String createQueryUrl(@NonNull String query, @NonNull String language); + + protected abstract void parseResults(@NonNull InputStream inputStream, @NonNull List results) throws Exception; + + BaseSuggestionsModel(@NonNull Application application, @NonNull String encoding) { + mEncoding = encoding; + mLanguage = getLanguage(); + File suggestionsCache = new File(application.getCacheDir(), "suggestion_responses"); + mHttpClient = new OkHttpClient.Builder() + .cache(new Cache(suggestionsCache, FileUtils.megabytesToBytes(1))) + .addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR) + .build(); + mCacheControl = new CacheControl.Builder().maxStale(1, TimeUnit.DAYS).build(); + } + + @NonNull + private static String getLanguage() { + String language = Locale.getDefault().getLanguage(); + if (TextUtils.isEmpty(language)) { + language = DEFAULT_LANGUAGE; + } + return language; + } + + @NonNull + List getResults(@NonNull String query) { + List filter = new ArrayList<>(5); + try { + query = URLEncoder.encode(query, mEncoding); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Unable to encode the URL", e); + } + InputStream inputStream = downloadSuggestionsForQuery(query, mLanguage); + if (inputStream == null) { + // There are no suggestions for this query, return an empty list. + return filter; + } + try { + parseResults(inputStream, filter); + } catch (Exception e) { + Log.e(TAG, "Unable to parse results", e); + return filter; + } finally { + Utils.close(inputStream); + } + + return filter; + } + + /** + * This method downloads the search suggestions for the specific query. + * NOTE: This is a blocking operation, do not getResults on the UI thread. + * + * @param query the query to get suggestions for + * @return the cache file containing the suggestions + */ + @Nullable + private InputStream downloadSuggestionsForQuery(@NonNull String query, @NonNull String language) { + String queryUrl = createQueryUrl(query, language); + + try { + URL url = new URL(queryUrl); + + // OkHttp automatically gzips requests + Request suggestionsRequest = new Request.Builder().url(url) + .addHeader("Accept-Charset", mEncoding) + .cacheControl(mCacheControl) + .build(); + + Response suggestionsResponse = mHttpClient.newCall(suggestionsRequest).execute(); + + ResponseBody responseBody = suggestionsResponse.body(); + return responseBody != null ? responseBody.byteStream() : null; + } catch (IOException exception) { + Log.e(TAG, "Problem getting search suggestions", exception); + } + + return null; + } + + private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() { + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + Response originalResponse = chain.proceed(chain.request()); + return originalResponse.newBuilder() + .header("cache-control", "max-age=" + INTERVAL_DAY + ", max-stale=" + INTERVAL_DAY) + .build(); + } + }; + +} diff --git a/app/src/main/java/acr/browser/lightning/search/BaseSuggestionsTask.java b/app/src/main/java/acr/browser/lightning/search/BaseSuggestionsTask.java deleted file mode 100644 index 67fad33ec..000000000 --- a/app/src/main/java/acr/browser/lightning/search/BaseSuggestionsTask.java +++ /dev/null @@ -1,170 +0,0 @@ -package acr.browser.lightning.search; - -import android.app.Application; -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.util.Log; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.TimeUnit; -import java.util.zip.GZIPInputStream; - -import javax.net.ssl.HttpsURLConnection; - -import acr.browser.lightning.database.HistoryItem; -import acr.browser.lightning.utils.Utils; - -abstract class BaseSuggestionsTask { - - private static final String TAG = BaseSuggestionsTask.class.getSimpleName(); - - static final int MAX_RESULTS = 5; - private static final long INTERVAL_DAY = TimeUnit.DAYS.toMillis(1); - private static final String DEFAULT_LANGUAGE = "en"; - @Nullable private static String sLanguage; - @NonNull private final SuggestionsResult mResultCallback; - @NonNull private final Application mApplication; - @NonNull private String mQuery; - - protected abstract String getQueryUrl(@NonNull String query, @NonNull String language); - - protected abstract void parseResults(FileInputStream inputStream, List results) throws Exception; - - protected abstract String getEncoding(); - - BaseSuggestionsTask(@NonNull String query, - @NonNull Application application, - @NonNull SuggestionsResult callback) { - mQuery = query; - mResultCallback = callback; - mApplication = application; - } - - @NonNull - private static synchronized String getLanguage() { - if (sLanguage == null) { - sLanguage = Locale.getDefault().getLanguage(); - } - if (TextUtils.isEmpty(sLanguage)) { - sLanguage = DEFAULT_LANGUAGE; - } - return sLanguage; - } - - void run() { - List filter = new ArrayList<>(5); - try { - mQuery = URLEncoder.encode(mQuery, getEncoding()); - } catch (UnsupportedEncodingException e) { - Log.e(TAG, "Unable to encode the URL", e); - } - File cache = downloadSuggestionsForQuery(mQuery, getLanguage(), mApplication); - if (!cache.exists()) { - post(filter); - return; - } - FileInputStream fileInput = null; - try { - fileInput = new FileInputStream(cache); - parseResults(fileInput, filter); - } catch (Exception e) { - post(filter); - Log.e(TAG, "Unable to parse results", e); - return; - } finally { - Utils.close(fileInput); - } - post(filter); - } - - private void post(@NonNull List result) { - mResultCallback.resultReceived(result); - } - - /** - * This method downloads the search suggestions for the specific query. - * NOTE: This is a blocking operation, do not run on the UI thread. - * - * @param query the query to get suggestions for - * @return the cache file containing the suggestions - */ - @NonNull - private File downloadSuggestionsForQuery(@NonNull String query, String language, @NonNull Application app) { - String queryUrl = getQueryUrl(query, language); - File cacheFile = new File(app.getCacheDir(), queryUrl.hashCode() + SuggestionsAdapter.CACHE_FILE_TYPE); - if (System.currentTimeMillis() - INTERVAL_DAY < cacheFile.lastModified()) { - return cacheFile; - } - if (!isNetworkConnected(app)) { - return cacheFile; - } - InputStream in = null; - FileOutputStream fos = null; - try { - URL url = new URL(queryUrl); - HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); - connection.setDoInput(true); - connection.setRequestProperty("Accept-Encoding", "gzip"); - connection.setRequestProperty("Accept-Charset", getEncoding()); - connection.connect(); - if (connection.getResponseCode() >= HttpURLConnection.HTTP_MULT_CHOICE || - connection.getResponseCode() < HttpURLConnection.HTTP_OK) { - Log.e(TAG, "Search API Responded with code: " + connection.getResponseCode()); - connection.disconnect(); - return cacheFile; - } - - in = connection.getInputStream(); - - if (in != null) { - in = new GZIPInputStream(in); - //noinspection IOResourceOpenedButNotSafelyClosed - fos = new FileOutputStream(cacheFile); - int buffer; - while ((buffer = in.read()) != -1) { - fos.write(buffer); - } - fos.flush(); - } - connection.disconnect(); - cacheFile.setLastModified(System.currentTimeMillis()); - } catch (Exception e) { - Log.w(TAG, "Problem getting search suggestions", e); - } finally { - Utils.close(in); - Utils.close(fos); - } - return cacheFile; - } - - private static boolean isNetworkConnected(@NonNull Context context) { - NetworkInfo networkInfo = getActiveNetworkInfo(context); - return networkInfo != null && networkInfo.isConnected(); - } - - @Nullable - private static NetworkInfo getActiveNetworkInfo(@NonNull Context context) { - ConnectivityManager connectivity = (ConnectivityManager) context - .getApplicationContext() - .getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivity == null) { - return null; - } - return connectivity.getActiveNetworkInfo(); - } - -} diff --git a/app/src/main/java/acr/browser/lightning/search/DuckSuggestionsTask.java b/app/src/main/java/acr/browser/lightning/search/DuckSuggestionsModel.java similarity index 60% rename from app/src/main/java/acr/browser/lightning/search/DuckSuggestionsTask.java rename to app/src/main/java/acr/browser/lightning/search/DuckSuggestionsModel.java index cec467fca..b07b3039f 100644 --- a/app/src/main/java/acr/browser/lightning/search/DuckSuggestionsTask.java +++ b/app/src/main/java/acr/browser/lightning/search/DuckSuggestionsModel.java @@ -6,33 +6,32 @@ import org.json.JSONArray; import org.json.JSONObject; -import java.io.FileInputStream; +import java.io.InputStream; import java.util.List; import acr.browser.lightning.R; import acr.browser.lightning.database.HistoryItem; import acr.browser.lightning.utils.FileUtils; -final class DuckSuggestionsTask extends BaseSuggestionsTask { +final class DuckSuggestionsModel extends BaseSuggestionsModel { - private static final String ENCODING = "UTF-8"; + @NonNull private static final String ENCODING = "UTF-8"; @NonNull private final String mSearchSubtitle; - DuckSuggestionsTask(@NonNull String query, - @NonNull Application application, - @NonNull SuggestionsResult callback) { - super(query, application, callback); + DuckSuggestionsModel(@NonNull Application application) { + super(application, ENCODING); mSearchSubtitle = application.getString(R.string.suggestion); } + @NonNull @Override - protected String getQueryUrl(@NonNull String query, @NonNull String language) { + protected String createQueryUrl(@NonNull String query, @NonNull String language) { return "https://duckduckgo.com/ac/?q=" + query; } @Override - protected void parseResults(FileInputStream inputStream, List results) throws Exception { - String content = FileUtils.readStringFromFile(inputStream, ENCODING); + protected void parseResults(@NonNull InputStream inputStream, @NonNull List results) throws Exception { + String content = FileUtils.readStringFromStream(inputStream, ENCODING); JSONArray jsonArray = new JSONArray(content); int counter = 0; for (int n = 0, size = jsonArray.length(); n < size; n++) { @@ -47,9 +46,4 @@ protected void parseResults(FileInputStream inputStream, List resul } } - @Override - protected String getEncoding() { - return ENCODING; - } - } diff --git a/app/src/main/java/acr/browser/lightning/search/GoogleSuggestionsTask.java b/app/src/main/java/acr/browser/lightning/search/GoogleSuggestionsModel.java similarity index 69% rename from app/src/main/java/acr/browser/lightning/search/GoogleSuggestionsTask.java rename to app/src/main/java/acr/browser/lightning/search/GoogleSuggestionsModel.java index d68da9a7f..ed892a094 100644 --- a/app/src/main/java/acr/browser/lightning/search/GoogleSuggestionsTask.java +++ b/app/src/main/java/acr/browser/lightning/search/GoogleSuggestionsModel.java @@ -9,36 +9,34 @@ import org.xmlpull.v1.XmlPullParserFactory; import java.io.BufferedInputStream; -import java.io.FileInputStream; +import java.io.InputStream; import java.util.List; import acr.browser.lightning.R; import acr.browser.lightning.database.HistoryItem; -class GoogleSuggestionsTask extends BaseSuggestionsTask { +class GoogleSuggestionsModel extends BaseSuggestionsModel { - private static final String ENCODING = "ISO-8859-1"; + @NonNull private static final String ENCODING = "ISO-8859-1"; @Nullable private static XmlPullParser sXpp; @NonNull private final String mSearchSubtitle; - GoogleSuggestionsTask(@NonNull String query, - @NonNull Application application, - @NonNull SuggestionsResult callback) { - super(query, application, callback); + GoogleSuggestionsModel(@NonNull Application application) { + super(application, ENCODING); mSearchSubtitle = application.getString(R.string.suggestion); } @NonNull - protected String getQueryUrl(@NonNull String query, @NonNull String language) { - return "https://suggestqueries.google.com/complete/search?output=toolbar&hl=" + protected String createQueryUrl(@NonNull String query, @NonNull String language) { + return "https://suggestqueries.google.com/complete/search?output=toolbar&oe=latin1&hl=" + language + "&q=" + query; } @Override - protected void parseResults(FileInputStream inputStream, List results) throws Exception { - BufferedInputStream fileInput = new BufferedInputStream(inputStream); + protected void parseResults(@NonNull InputStream inputStream, @NonNull List results) throws Exception { + BufferedInputStream bufferedInput = new BufferedInputStream(inputStream); XmlPullParser parser = getParser(); - parser.setInput(fileInput, ENCODING); + parser.setInput(bufferedInput, ENCODING); int eventType = parser.getEventType(); int counter = 0; while (eventType != XmlPullParser.END_DOCUMENT) { @@ -55,11 +53,6 @@ protected void parseResults(FileInputStream inputStream, List resul } } - @Override - protected String getEncoding() { - return ENCODING; - } - @NonNull private static synchronized XmlPullParser getParser() throws XmlPullParserException { if (sXpp == null) { diff --git a/app/src/main/java/acr/browser/lightning/search/SuggestionsAdapter.java b/app/src/main/java/acr/browser/lightning/search/SuggestionsAdapter.java index f1dfc1647..a14a9192b 100644 --- a/app/src/main/java/acr/browser/lightning/search/SuggestionsAdapter.java +++ b/app/src/main/java/acr/browser/lightning/search/SuggestionsAdapter.java @@ -15,12 +15,15 @@ import android.widget.ImageView; import android.widget.TextView; -import com.anthonycr.bonsai.Action; -import com.anthonycr.bonsai.Observable; -import com.anthonycr.bonsai.OnSubscribe; +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableAction; +import com.anthonycr.bonsai.CompletableSubscriber; import com.anthonycr.bonsai.Scheduler; import com.anthonycr.bonsai.Schedulers; -import com.anthonycr.bonsai.Subscriber; +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleOnSubscribe; +import com.anthonycr.bonsai.SingleSubscriber; import java.io.File; import java.io.FilenameFilter; @@ -30,23 +33,23 @@ import java.util.Iterator; import java.util.List; import java.util.Locale; -import java.util.concurrent.TimeUnit; import javax.inject.Inject; import acr.browser.lightning.R; import acr.browser.lightning.app.BrowserApp; -import acr.browser.lightning.database.BookmarkManager; -import acr.browser.lightning.database.HistoryDatabase; import acr.browser.lightning.database.HistoryItem; +import acr.browser.lightning.database.bookmark.BookmarkModel; +import acr.browser.lightning.database.history.HistoryModel; import acr.browser.lightning.preference.PreferenceManager; +import acr.browser.lightning.utils.Preconditions; import acr.browser.lightning.utils.ThemeUtils; public class SuggestionsAdapter extends BaseAdapter implements Filterable { private static final Scheduler FILTER_SCHEDULER = Schedulers.newSingleThreadedScheduler(); - static final String CACHE_FILE_TYPE = ".sgg"; + private static final String CACHE_FILE_TYPE = ".sgg"; private final List mFilteredList = new ArrayList<>(5); @@ -62,9 +65,9 @@ public class SuggestionsAdapter extends BaseAdapter implements Filterable { private final Comparator mFilterComparator = new SuggestionsComparator(); - @Inject HistoryDatabase mDatabaseHandler; - @Inject BookmarkManager mBookmarkManager; + @Inject BookmarkModel mBookmarkManager; @Inject PreferenceManager mPreferenceManager; + @Inject Application mApplication; private final List mAllBookmarks = new ArrayList<>(5); @@ -94,20 +97,21 @@ public void refreshPreferences() { } public void clearCache() { - Schedulers.io().execute(new ClearCacheRunnable(BrowserApp.get(mContext))); + // We don't need these cache files anymore + Schedulers.io().execute(new ClearCacheRunnable(mApplication)); } public void refreshBookmarks() { - Observable.create(new Action() { - @Override - public void onSubscribe(@NonNull Subscriber subscriber) { - mAllBookmarks.clear(); - mAllBookmarks.addAll(mBookmarkManager.getAllBookmarks(true)); - - subscriber.onComplete(); - } - }).subscribeOn(Schedulers.io()) - .subscribe(); + mBookmarkManager.getAllBookmarks() + .subscribeOn(Schedulers.io()) + .subscribe(new SingleOnSubscribe>() { + @Override + public void onItem(@Nullable List item) { + Preconditions.checkNonNull(item); + mAllBookmarks.clear(); + mAllBookmarks.addAll(item); + } + }); } @Override @@ -115,6 +119,7 @@ public int getCount() { return mFilteredList.size(); } + @Nullable @Override public Object getItem(int position) { if (position > mFilteredList.size() || position < 0) { @@ -136,14 +141,15 @@ private static class SuggestionHolder { mImage = (ImageView) view.findViewById(R.id.suggestionIcon); } - final ImageView mImage; - final TextView mTitle; - final TextView mUrl; + @NonNull final ImageView mImage; + @NonNull final TextView mTitle; + @NonNull final TextView mUrl; } + @Nullable @Override - public View getView(int position, View convertView, ViewGroup parent) { + public View getView(int position, @Nullable View convertView, ViewGroup parent) { SuggestionHolder holder; if (convertView == null) { @@ -188,21 +194,22 @@ public View getView(int position, View convertView, ViewGroup parent) { return convertView; } + @NonNull @Override public Filter getFilter() { return new SearchFilter(this); } - private synchronized void publishResults(List list) { + private synchronized void publishResults(@NonNull List list) { mFilteredList.clear(); mFilteredList.addAll(list); notifyDataSetChanged(); } private void clearSuggestions() { - Observable.create(new Action() { + Completable.create(new CompletableAction() { @Override - public void onSubscribe(@NonNull Subscriber subscriber) { + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { mBookmarks.clear(); mHistory.clear(); mSuggestions.clear(); @@ -216,9 +223,9 @@ public void onSubscribe(@NonNull Subscriber subscriber) { private void combineResults(final @Nullable List bookmarkList, final @Nullable List historyList, final @Nullable List suggestionList) { - Observable.create(new Action>() { + Single.create(new SingleAction>() { @Override - public void onSubscribe(@NonNull Subscriber> subscriber) { + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { List list = new ArrayList<>(5); if (bookmarkList != null) { mBookmarks.clear(); @@ -251,25 +258,25 @@ public void onSubscribe(@NonNull Subscriber> subscriber) { } Collections.sort(list, mFilterComparator); - subscriber.onNext(list); + subscriber.onItem(list); subscriber.onComplete(); } }).subscribeOn(FILTER_SCHEDULER) .observeOn(Schedulers.main()) - .subscribe(new OnSubscribe>() { + .subscribe(new SingleOnSubscribe>() { @Override - public void onNext(@Nullable List item) { + public void onItem(@Nullable List item) { + Preconditions.checkNonNull(item); publishResults(item); } }); - } @NonNull - private Observable> getBookmarksForQuery(@NonNull final String query) { - return Observable.create(new Action>() { + private Single> getBookmarksForQuery(@NonNull final String query) { + return Single.create(new SingleAction>() { @Override - public void onSubscribe(@NonNull Subscriber> subscriber) { + public void onSubscribe(@NonNull SingleSubscriber> subscriber) { List bookmarks = new ArrayList<>(5); int counter = 0; for (int n = 0; n < mAllBookmarks.size(); n++) { @@ -285,41 +292,25 @@ public void onSubscribe(@NonNull Subscriber> subscriber) { counter++; } } - subscriber.onNext(bookmarks); + subscriber.onItem(bookmarks); subscriber.onComplete(); } }); } @NonNull - private Observable> getSuggestionsForQuery(@NonNull final String query) { + private Single> getSuggestionsForQuery(@NonNull final String query) { if (mSuggestionChoice == PreferenceManager.Suggestion.SUGGESTION_GOOGLE) { - return SuggestionsManager.getObservable(query, mContext, SuggestionsManager.Source.GOOGLE); + return SuggestionsManager.createGoogleQueryObservable(query, mApplication); } else if (mSuggestionChoice == PreferenceManager.Suggestion.SUGGESTION_DUCK) { - return SuggestionsManager.getObservable(query, mContext, SuggestionsManager.Source.DUCK); + return SuggestionsManager.createDuckQueryObservable(query, mApplication); + } else if (mSuggestionChoice == PreferenceManager.Suggestion.SUGGESTION_BAIDU) { + return SuggestionsManager.createBaiduQueryObservable(query, mApplication); } else { - return Observable.create(new Action>() { - @Override - public void onSubscribe(@NonNull Subscriber> subscriber) { - //TODO add an Observable.empty() method to generate an empty Observable - subscriber.onComplete(); - } - }); + return Single.empty(); } } - @NonNull - private Observable> getHistoryForQuery(@NonNull final String query) { - return Observable.create(new Action>() { - @Override - public void onSubscribe(@NonNull Subscriber> subscriber) { - List historyList = mDatabaseHandler.findItemsContaining(query); - subscriber.onNext(historyList); - subscriber.onComplete(); - } - }); - } - private boolean shouldRequestNetwork() { return !mIsIncognito && mSuggestionChoice != PreferenceManager.Suggestion.SUGGESTION_NONE; } @@ -332,8 +323,9 @@ private static class SearchFilter extends Filter { mSuggestionsAdapter = suggestionsAdapter; } + @NonNull @Override - protected FilterResults performFiltering(CharSequence constraint) { + protected FilterResults performFiltering(@Nullable CharSequence constraint) { FilterResults results = new FilterResults(); if (constraint == null || constraint.length() == 0) { mSuggestionsAdapter.clearSuggestions(); @@ -345,9 +337,9 @@ protected FilterResults performFiltering(CharSequence constraint) { mSuggestionsAdapter.getSuggestionsForQuery(query) .subscribeOn(Schedulers.worker()) .observeOn(Schedulers.main()) - .subscribe(new OnSubscribe>() { + .subscribe(new SingleOnSubscribe>() { @Override - public void onNext(@Nullable List item) { + public void onItem(@Nullable List item) { mSuggestionsAdapter.combineResults(null, null, item); } }); @@ -356,19 +348,19 @@ public void onNext(@Nullable List item) { mSuggestionsAdapter.getBookmarksForQuery(query) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.main()) - .subscribe(new OnSubscribe>() { + .subscribe(new SingleOnSubscribe>() { @Override - public void onNext(@Nullable List item) { + public void onItem(@Nullable List item) { mSuggestionsAdapter.combineResults(item, null, null); } }); - mSuggestionsAdapter.getHistoryForQuery(query) + HistoryModel.findHistoryItemsContaining(query) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.main()) - .subscribe(new OnSubscribe>() { + .subscribe(new SingleOnSubscribe>() { @Override - public void onNext(@Nullable List item) { + public void onItem(@Nullable List item) { mSuggestionsAdapter.combineResults(null, item, null); } }); @@ -376,8 +368,9 @@ public void onNext(@Nullable List item) { return results; } + @NonNull @Override - public CharSequence convertResultToString(Object resultValue) { + public CharSequence convertResultToString(@NonNull Object resultValue) { return ((HistoryItem) resultValue).getUrl(); } @@ -400,12 +393,9 @@ private static class ClearCacheRunnable implements Runnable { public void run() { File dir = new File(app.getCacheDir().toString()); String[] fileList = dir.list(new NameFilter()); - long earliestTimeAllowed = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1); for (String fileName : fileList) { File file = new File(dir.getPath() + fileName); - if (earliestTimeAllowed > file.lastModified()) { - file.delete(); - } + file.delete(); } } diff --git a/app/src/main/java/acr/browser/lightning/search/SuggestionsManager.java b/app/src/main/java/acr/browser/lightning/search/SuggestionsManager.java index 3ea94d7ed..13edb8c81 100644 --- a/app/src/main/java/acr/browser/lightning/search/SuggestionsManager.java +++ b/app/src/main/java/acr/browser/lightning/search/SuggestionsManager.java @@ -1,54 +1,64 @@ package acr.browser.lightning.search; -import android.content.Context; +import android.app.Application; import android.support.annotation.NonNull; -import com.anthonycr.bonsai.Action; -import com.anthonycr.bonsai.Observable; -import com.anthonycr.bonsai.Subscriber; +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleSubscriber; import java.util.List; -import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.database.HistoryItem; class SuggestionsManager { - public enum Source { - GOOGLE, - DUCK - } - private static volatile boolean sIsTaskExecuting; static boolean isRequestInProgress() { return sIsTaskExecuting; } - static Observable> getObservable(@NonNull final String query, @NonNull final Context context, @NonNull final Source source) { - return Observable.create(new Action>() { + @NonNull + static Single> createGoogleQueryObservable(@NonNull final String query, + @NonNull final Application application) { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull final SingleSubscriber> subscriber) { + sIsTaskExecuting = true; + List results = new GoogleSuggestionsModel(application).getResults(query); + subscriber.onItem(results); + subscriber.onComplete(); + sIsTaskExecuting = false; + } + }); + } + + @NonNull + static Single> createBaiduQueryObservable(@NonNull final String query, + @NonNull final Application application) { + return Single.create(new SingleAction>() { + @Override + public void onSubscribe(@NonNull final SingleSubscriber> subscriber) { + sIsTaskExecuting = true; + List results = new BaiduSuggestionsModel(application).getResults(query); + subscriber.onItem(results); + subscriber.onComplete(); + sIsTaskExecuting = false; + } + }); + } + + @NonNull + static Single> createDuckQueryObservable(@NonNull final String query, + @NonNull final Application application) { + return Single.create(new SingleAction>() { @Override - public void onSubscribe(@NonNull final Subscriber> subscriber) { + public void onSubscribe(@NonNull final SingleSubscriber> subscriber) { sIsTaskExecuting = true; - switch (source) { - case GOOGLE: - new GoogleSuggestionsTask(query, BrowserApp.get(context), new SuggestionsResult() { - @Override - public void resultReceived(@NonNull List searchResults) { - subscriber.onNext(searchResults); - subscriber.onComplete(); - } - }).run(); - break; - case DUCK: - new DuckSuggestionsTask(query, BrowserApp.get(context), new SuggestionsResult() { - @Override - public void resultReceived(@NonNull List searchResults) { - subscriber.onNext(searchResults); - subscriber.onComplete(); - } - }).run(); - } + List results = new DuckSuggestionsModel(application).getResults(query); + subscriber.onItem(results); + subscriber.onComplete(); sIsTaskExecuting = false; } }); diff --git a/app/src/main/java/acr/browser/lightning/search/SuggestionsResult.java b/app/src/main/java/acr/browser/lightning/search/SuggestionsResult.java deleted file mode 100644 index 63e420cdc..000000000 --- a/app/src/main/java/acr/browser/lightning/search/SuggestionsResult.java +++ /dev/null @@ -1,21 +0,0 @@ -package acr.browser.lightning.search; - -import android.support.annotation.NonNull; - -import java.util.List; - -import acr.browser.lightning.database.HistoryItem; - -interface SuggestionsResult { - - /** - * Called when the search suggestions have - * been retrieved from the server. - * - * @param searchResults the results, a valid - * list of results. May - * be empty. - */ - void resultReceived(@NonNull List searchResults); - -} diff --git a/app/src/main/java/acr/browser/lightning/utils/AdBlock.java b/app/src/main/java/acr/browser/lightning/utils/AdBlock.java index 543b90abf..0fd6fc07a 100644 --- a/app/src/main/java/acr/browser/lightning/utils/AdBlock.java +++ b/app/src/main/java/acr/browser/lightning/utils/AdBlock.java @@ -1,11 +1,16 @@ package acr.browser.lightning.utils; -import android.content.Context; +import android.app.Application; import android.content.res.AssetManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; +import com.anthonycr.bonsai.Completable; +import com.anthonycr.bonsai.CompletableAction; +import com.anthonycr.bonsai.CompletableSubscriber; +import com.anthonycr.bonsai.Schedulers; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -17,8 +22,7 @@ import javax.inject.Inject; import javax.inject.Singleton; -import acr.browser.lightning.app.BrowserApp; -import acr.browser.lightning.constant.Constants; +import acr.browser.lightning.BuildConfig; import acr.browser.lightning.preference.PreferenceManager; @Singleton @@ -34,16 +38,18 @@ public class AdBlock { private static final String TAB = "\t"; private static final String SPACE = " "; private static final String EMPTY = ""; - private final Set mBlockedDomainsList = new HashSet<>(); - private boolean mBlockAds; - @Inject PreferenceManager mPreferenceManager; + @NonNull private final Set mBlockedDomainsList = new HashSet<>(); + @NonNull private final PreferenceManager mPreferenceManager; + @NonNull private final Application mApplication; + private boolean mBlockAds; @Inject - public AdBlock(@NonNull Context context) { - BrowserApp.getAppComponent().inject(this); - if (mBlockedDomainsList.isEmpty() && Constants.FULL_VERSION) { - loadHostsFile(context); + AdBlock(@NonNull Application application, @NonNull PreferenceManager preferenceManager) { + mApplication = application; + mPreferenceManager = preferenceManager; + if (mBlockedDomainsList.isEmpty() && BuildConfig.FULL_VERSION) { + loadHostsFile().subscribeOn(Schedulers.io()).subscribe(); } mBlockAds = mPreferenceManager.getAdBlockEnabled(); } @@ -52,31 +58,6 @@ public void updatePreference() { mBlockAds = mPreferenceManager.getAdBlockEnabled(); } - private void loadBlockedDomainsList(@NonNull final Context context) { - BrowserApp.getTaskThread().execute(new Runnable() { - - @Override - public void run() { - AssetManager asset = context.getAssets(); - BufferedReader reader = null; - try { - //noinspection IOResourceOpenedButNotSafelyClosed - reader = new BufferedReader(new InputStreamReader( - asset.open(BLOCKED_DOMAINS_LIST_FILE_NAME))); - String line; - while ((line = reader.readLine()) != null) { - mBlockedDomainsList.add(line.trim()); - } - } catch (IOException e) { - Log.wtf(TAG, "Reading blocked domains list from file '" - + BLOCKED_DOMAINS_LIST_FILE_NAME + "' failed.", e); - } finally { - Utils.close(reader); - } - } - }); - } - /** * a method that determines if the given URL is an ad or not. It performs * a search of the URL's domain on the blocked domain hash set. @@ -128,48 +109,66 @@ private static String getDomainName(@NonNull String url) throws URISyntaxExcepti } /** - * This method reads through a hosts file and extracts the domains that should + * This Completable reads through a hosts file and extracts the domains that should * be redirected to localhost (a.k.a. IP address 127.0.0.1). It can handle files that - * simply have a list of hostnames to block, or it can handle a full blown hosts file. + * simply have a list of host names to block, or it can handle a full blown hosts file. * It will strip out comments, references to the base IP address and just extract the - * domains to be used + * domains to be used. * - * @param context the context needed to read the file + * @return a Completable that will load the hosts file into memory. */ - private void loadHostsFile(@NonNull final Context context) { - BrowserApp.getTaskThread().execute(new Runnable() { - + @NonNull + private Completable loadHostsFile() { + return Completable.create(new CompletableAction() { @Override - public void run() { - AssetManager asset = context.getAssets(); + public void onSubscribe(@NonNull CompletableSubscriber subscriber) { + AssetManager asset = mApplication.getAssets(); BufferedReader reader = null; + //noinspection TryFinallyCanBeTryWithResources try { - //noinspection IOResourceOpenedButNotSafelyClosed reader = new BufferedReader(new InputStreamReader( asset.open(BLOCKED_DOMAINS_LIST_FILE_NAME))); + StringBuilder lineBuilder = new StringBuilder(); String line; + long time = System.currentTimeMillis(); + while ((line = reader.readLine()) != null) { - if (!line.isEmpty() && !line.startsWith(COMMENT)) { - line = line.replace(LOCAL_IP_V4, EMPTY) - .replace(LOCAL_IP_V4_ALT, EMPTY) - .replace(LOCAL_IP_V6, EMPTY) - .replace(TAB, EMPTY); - int comment = line.indexOf(COMMENT); + lineBuilder.append(line); + + if (!StringBuilderUtils.isEmpty(lineBuilder) && + !StringBuilderUtils.startsWith(lineBuilder, COMMENT)) { + StringBuilderUtils.replace(lineBuilder, LOCAL_IP_V4, EMPTY); + StringBuilderUtils.replace(lineBuilder, LOCAL_IP_V4_ALT, EMPTY); + StringBuilderUtils.replace(lineBuilder, LOCAL_IP_V6, EMPTY); + StringBuilderUtils.replace(lineBuilder, TAB, EMPTY); + + int comment = lineBuilder.indexOf(COMMENT); if (comment >= 0) { - line = line.substring(0, comment); + lineBuilder.replace(comment, lineBuilder.length(), EMPTY); } - line = line.trim(); - if (!line.isEmpty() && !line.equals(LOCALHOST)) { - while (line.contains(SPACE)) { - int space = line.indexOf(SPACE); - String host = line.substring(0, space); - mBlockedDomainsList.add(host.trim()); - line = line.substring(space, line.length()).trim(); + + StringBuilderUtils.trim(lineBuilder); + + if (!StringBuilderUtils.isEmpty(lineBuilder) && + !StringBuilderUtils.equals(lineBuilder, LOCALHOST)) { + while (StringBuilderUtils.contains(lineBuilder, SPACE)) { + int space = lineBuilder.indexOf(SPACE); + StringBuilder partial = StringBuilderUtils.substring(lineBuilder, 0, space); + StringBuilderUtils.trim(partial); + + String partialLine = partial.toString(); + mBlockedDomainsList.add(partialLine); + StringBuilderUtils.replace(lineBuilder, partialLine, EMPTY); + StringBuilderUtils.trim(lineBuilder); + } + if (lineBuilder.length() > 0) { + mBlockedDomainsList.add(lineBuilder.toString()); } - mBlockedDomainsList.add(line.trim()); } } + lineBuilder.setLength(0); } + Log.d(TAG, "Loaded ad list in: " + (System.currentTimeMillis() - time) + " ms"); } catch (IOException e) { Log.wtf(TAG, "Reading blocked domains list from file '" + BLOCKED_DOMAINS_LIST_FILE_NAME + "' failed.", e); @@ -179,4 +178,5 @@ public void run() { } }); } + } diff --git a/app/src/main/java/acr/browser/lightning/utils/DrawableUtils.java b/app/src/main/java/acr/browser/lightning/utils/DrawableUtils.java index 91d76a400..19e426cf4 100644 --- a/app/src/main/java/acr/browser/lightning/utils/DrawableUtils.java +++ b/app/src/main/java/acr/browser/lightning/utils/DrawableUtils.java @@ -1,7 +1,5 @@ package acr.browser.lightning.utils; -import android.content.Context; -import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; @@ -11,10 +9,8 @@ import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Build; -import android.support.annotation.AttrRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.util.TypedValue; import android.view.View; public class DrawableUtils { @@ -78,16 +74,6 @@ public static int mixColor(float fraction, int startValue, int endValue) { (startB + (int) (fraction * (endB - startB))); } - public static Drawable resolveDrawableAttribute(@NonNull Context context, @AttrRes int res) { - int[] attribute = new int[]{res}; - int indexOfAttrTextSize = 0; - TypedValue typedValue = new TypedValue(); - TypedArray a = context.obtainStyledAttributes(typedValue.data, attribute); - Drawable drawable = a.getDrawable(indexOfAttrTextSize); - a.recycle(); - return drawable; - } - public static void setBackground(@NonNull View view, @Nullable Drawable drawable) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { view.setBackground(drawable); diff --git a/app/src/main/java/acr/browser/lightning/utils/FileUtils.java b/app/src/main/java/acr/browser/lightning/utils/FileUtils.java index 2f572ad82..0ac81358c 100644 --- a/app/src/main/java/acr/browser/lightning/utils/FileUtils.java +++ b/app/src/main/java/acr/browser/lightning/utils/FileUtils.java @@ -8,6 +8,8 @@ import android.support.annotation.Nullable; import android.util.Log; +import com.anthonycr.bonsai.Schedulers; + import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -18,15 +20,14 @@ import java.io.InputStreamReader; import java.io.PrintStream; -import acr.browser.lightning.app.BrowserApp; -import acr.browser.lightning.constant.Constants; - /** * A utility class containing helpful methods * pertaining to file storage. */ public class FileUtils { + private static final String TAG = "FileUtils"; + /** * Writes a bundle to persistent storage in the files directory * using the specified file name. This method is a blocking @@ -37,7 +38,7 @@ public class FileUtils { * @param name the name of the file to store the bundle in. */ public static void writeBundleToStorage(final @NonNull Application app, final Bundle bundle, final @NonNull String name) { - BrowserApp.getIOThread().execute(new Runnable() { + Schedulers.io().execute(new Runnable() { @Override public void run() { File outputFile = new File(app.getFilesDir(), name); @@ -51,7 +52,7 @@ public void run() { outputStream.flush(); parcel.recycle(); } catch (IOException e) { - Log.e(Constants.TAG, "Unable to write bundle to storage"); + Log.e(TAG, "Unable to write bundle to storage"); } finally { Utils.close(outputStream); } @@ -103,7 +104,7 @@ public static Bundle readBundleFromStorage(@NonNull Application app, @NonNull St parcel.recycle(); return out; } catch (FileNotFoundException e) { - Log.e(Constants.TAG, "Unable to read bundle from storage"); + Log.e(TAG, "Unable to read bundle from storage"); } catch (IOException e) { e.printStackTrace(); } finally { @@ -131,14 +132,15 @@ public static void writeCrashToStorage(@NonNull Throwable throwable) { throwable.printStackTrace(new PrintStream(outputStream)); outputStream.flush(); } catch (IOException e) { - Log.e(Constants.TAG, "Unable to write bundle to storage"); + Log.e(TAG, "Unable to write bundle to storage"); } finally { Utils.close(outputStream); } } @NonNull - public static String readStringFromFile(@NonNull InputStream inputStream, @NonNull String encoding) throws IOException { + public static String readStringFromStream(@NonNull InputStream inputStream, + @NonNull String encoding) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, encoding)); StringBuilder result = new StringBuilder(); String line; @@ -148,4 +150,14 @@ public static String readStringFromFile(@NonNull InputStream inputStream, @NonNu return result.toString(); } + /** + * Converts megabytes to bytes. + * + * @param megaBytes the number of megabytes. + * @return the converted bytes. + */ + public static long megabytesToBytes(long megaBytes) { + return megaBytes * 1024 * 1024; + } + } diff --git a/app/src/main/java/acr/browser/lightning/utils/IntentUtils.java b/app/src/main/java/acr/browser/lightning/utils/IntentUtils.java index 11cb21a6f..7f953f4eb 100644 --- a/app/src/main/java/acr/browser/lightning/utils/IntentUtils.java +++ b/app/src/main/java/acr/browser/lightning/utils/IntentUtils.java @@ -8,6 +8,7 @@ import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Build; +import android.os.FileUriExposedException; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; @@ -18,6 +19,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import acr.browser.lightning.R; import acr.browser.lightning.constant.Constants; public class IntentUtils { @@ -31,7 +33,7 @@ public class IntentUtils { "(?:http|https|file)://" + "|(?:inline|data|about|javascript):" + "|(?:.*:.*@)" + ')' + "(.*)"); - public IntentUtils(Activity activity) { + public IntentUtils(@NonNull Activity activity) { mActivity = activity; } @@ -63,7 +65,7 @@ public boolean startActivityForUrl(@Nullable WebView tab, @NonNull String url) { } } if (tab != null) { - intent.putExtra(Constants.INTENT_ORIGIN, 1); + intent.putExtra(Constants.INTENT_ORIGIN, tab.hashCode()); } Matcher m = ACCEPTED_URI_SCHEMA.matcher(url); @@ -74,8 +76,9 @@ public boolean startActivityForUrl(@Nullable WebView tab, @NonNull String url) { if (mActivity.startActivityIfNeeded(intent, -1)) { return true; } - } catch (ActivityNotFoundException ex) { - ex.printStackTrace(); + } catch (Exception exception) { + exception.printStackTrace(); + // TODO: 6/5/17 fix case where this could throw a FileUriExposedException due to file:// urls } return false; } @@ -84,7 +87,7 @@ public boolean startActivityForUrl(@Nullable WebView tab, @NonNull String url) { * Search for intent handlers that are specific to this URL aka, specialized * apps like google maps or youtube */ - private boolean isSpecializedHandlerAvailable(Intent intent) { + private boolean isSpecializedHandlerAvailable(@NonNull Intent intent) { PackageManager pm = mActivity.getPackageManager(); List handlers = pm.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER); @@ -112,4 +115,24 @@ private boolean isSpecializedHandlerAvailable(Intent intent) { } return false; } + + /** + * Shares a URL to the system. + * + * @param url the URL to share. If the URL is null + * or a special URL, no sharing will occur. + * @param title the title of the URL to share. This + * is optional. + */ + public void shareUrl(@Nullable String url, @Nullable String title) { + if (url != null && !UrlUtils.isSpecialUrl(url)) { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + if (title != null) { + shareIntent.putExtra(Intent.EXTRA_SUBJECT, title); + } + shareIntent.putExtra(Intent.EXTRA_TEXT, url); + mActivity.startActivity(Intent.createChooser(shareIntent, mActivity.getString(R.string.dialog_title_share))); + } + } } diff --git a/app/src/main/java/acr/browser/lightning/utils/KeyboardHelper.java b/app/src/main/java/acr/browser/lightning/utils/KeyboardHelper.java deleted file mode 100644 index d6ed3ec76..000000000 --- a/app/src/main/java/acr/browser/lightning/utils/KeyboardHelper.java +++ /dev/null @@ -1,67 +0,0 @@ -package acr.browser.lightning.utils; - -import android.graphics.Rect; -import android.support.annotation.NonNull; -import android.view.View; -import android.view.ViewTreeObserver; - -class KeyboardHelper { - - interface KeyboardListener { - /** - * Called when the visibility of the keyboard changes. - * Parameter tells whether the keyboard has been shown - * or hidden. - * - * @param visible true if the keyboard has been shown, - * false otherwise. - */ - void keyboardVisibilityChanged(boolean visible); - } - - @NonNull private final View mView; - private int mLastRight = -1; - private int mLastBottom = -1; - - /** - * Constructor - * - * @param view the view to listen on, should be - * the {@link android.R.id#content} view. - */ - public KeyboardHelper(@NonNull View view) { - mView = view; - } - - /** - * Registers a {@link KeyboardListener} to receive - * callbacks when the keyboard is shown for the specific - * view. The view used should be the content view as it - * will receive resize events from the system. - * - * @param listener the listener to register to receive events. - */ - public void registerKeyboardListener(@NonNull final KeyboardListener listener) { - mView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - Rect rect = new Rect(); - if (mLastBottom == -1) { - mLastBottom = rect.bottom; - } - if (mLastRight == -1) { - mLastRight = rect.right; - } - mView.getWindowVisibleDisplayFrame(rect); - if (mLastRight == rect.right && rect.bottom < mLastBottom) { - listener.keyboardVisibilityChanged(true); - } else if (mLastRight == rect.right && rect.bottom > mLastBottom) { - listener.keyboardVisibilityChanged(false); - } - mLastBottom = rect.bottom; - mLastRight = rect.right; - } - }); - } - -} diff --git a/app/src/main/java/acr/browser/lightning/utils/MemoryLeakUtils.java b/app/src/main/java/acr/browser/lightning/utils/MemoryLeakUtils.java index 0dac3e3c0..17d08e7ad 100644 --- a/app/src/main/java/acr/browser/lightning/utils/MemoryLeakUtils.java +++ b/app/src/main/java/acr/browser/lightning/utils/MemoryLeakUtils.java @@ -6,6 +6,7 @@ import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import android.view.View; import android.view.inputmethod.InputMethodManager; @@ -15,9 +16,9 @@ public class MemoryLeakUtils { - private static final String TAG = MemoryLeakUtils.class.getSimpleName(); + private static final String TAG = "MemoryLeakUtils"; - private static Method sFinishInputLocked = null; + @Nullable private static Method sFinishInputLocked = null; /** * Clears the mNextServedView and mServedView in diff --git a/app/src/main/java/acr/browser/lightning/utils/Preconditions.java b/app/src/main/java/acr/browser/lightning/utils/Preconditions.java index e9ffd8416..df91d7519 100644 --- a/app/src/main/java/acr/browser/lightning/utils/Preconditions.java +++ b/app/src/main/java/acr/browser/lightning/utils/Preconditions.java @@ -1,5 +1,7 @@ package acr.browser.lightning.utils; +import android.support.annotation.Nullable; + public class Preconditions { /** * Ensure that an object is not null @@ -8,7 +10,7 @@ public class Preconditions { * * @param object check nullness on this object. */ - public static void checkNonNull(Object object) { + public static void checkNonNull(@Nullable Object object) { if (object == null) { throw new RuntimeException("Object must not be null"); } diff --git a/app/src/main/java/acr/browser/lightning/utils/ProxyUtils.java b/app/src/main/java/acr/browser/lightning/utils/ProxyUtils.java index a444ea40a..9c7e8daf0 100644 --- a/app/src/main/java/acr/browser/lightning/utils/ProxyUtils.java +++ b/app/src/main/java/acr/browser/lightning/utils/ProxyUtils.java @@ -7,8 +7,6 @@ import android.support.v7.app.AlertDialog; import android.util.Log; -import com.squareup.otto.Bus; - import net.i2p.android.ui.I2PAndroidHelper; import javax.inject.Inject; @@ -20,17 +18,19 @@ import acr.browser.lightning.dialog.BrowserDialog; import acr.browser.lightning.preference.PreferenceManager; import info.guardianproject.netcipher.proxy.OrbotHelper; -import info.guardianproject.netcipher.web.WebkitProxy; +import info.guardianproject.netcipher.webkit.WebkitProxy; @Singleton public class ProxyUtils { + + private static final String TAG = "ProxyUtils"; + // Helper private static boolean mI2PHelperBound; private static boolean mI2PProxyInitialized; @Inject PreferenceManager mPreferences; @Inject I2PAndroidHelper mI2PHelper; - @Inject Bus mBus; @Inject public ProxyUtils() { @@ -114,8 +114,9 @@ private void initializeProxy(@NonNull Activity activity) { // We shouldn't be here return; case Constants.PROXY_ORBOT: - if (!OrbotHelper.isOrbotRunning(activity)) + if (!OrbotHelper.isOrbotRunning(activity)) { OrbotHelper.requestStartTor(activity); + } host = "localhost"; port = 8118; break; @@ -140,7 +141,7 @@ private void initializeProxy(@NonNull Activity activity) { try { WebkitProxy.setProxy(BrowserApp.class.getName(), activity.getApplicationContext(), null, host, port); } catch (Exception e) { - Log.d(Constants.TAG, "error enabling web proxying", e); + Log.d(TAG, "error enabling web proxying", e); } } @@ -192,7 +193,7 @@ public void onI2PAndroidBound() { } } - @Constants.PROXY + @Constants.Proxy public static int setProxyChoice(int choice, @NonNull Activity activity) { switch (choice) { case Constants.PROXY_ORBOT: @@ -201,9 +202,8 @@ public static int setProxyChoice(int choice, @NonNull Activity activity) { Utils.showSnackbar(activity, R.string.install_orbot); } break; - case Constants.PROXY_I2P: - I2PAndroidHelper ih = new I2PAndroidHelper(BrowserApp.get(activity)); + I2PAndroidHelper ih = new I2PAndroidHelper(activity.getApplication()); if (!ih.isI2PAndroidInstalled()) { choice = Constants.NO_PROXY; ih.promptToInstall(activity); diff --git a/app/src/main/java/acr/browser/lightning/utils/StringBuilderUtils.java b/app/src/main/java/acr/browser/lightning/utils/StringBuilderUtils.java new file mode 100644 index 000000000..3b2e56530 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/utils/StringBuilderUtils.java @@ -0,0 +1,129 @@ +package acr.browser.lightning.utils; + +import android.support.annotation.NonNull; + +/** + * A collection of utils methods for + * {@link StringBuilder} that provides + * API equality with the {@link String} + * API. + */ +public class StringBuilderUtils { + + private static final String SPACE = " "; + private static final String EMPTY = ""; + + /** + * Replace a string in a string + * builder with another string. + * + * @param stringBuilder the string builder. + * @param toReplace the string to replace. + * @param replacement the replacement string. + */ + public static void replace(@NonNull StringBuilder stringBuilder, + @NonNull String toReplace, + @NonNull String replacement) { + int index = stringBuilder.indexOf(toReplace); + if (index >= 0) { + stringBuilder.replace(index, index + toReplace.length(), replacement); + } + } + + /** + * Trims a string builder of + * any spaces at the beginning + * and end. + * + * @param stringBuilder the string builder. + */ + public static void trim(@NonNull StringBuilder stringBuilder) { + while (stringBuilder.indexOf(SPACE) == 0) { + stringBuilder.replace(0, 1, EMPTY); + } + + while (stringBuilder.lastIndexOf(SPACE) == (stringBuilder.length() - 1)) { + stringBuilder.replace(stringBuilder.length() - 1, stringBuilder.length(), EMPTY); + } + } + + /** + * Determines if the string builder is empty. + * + * @param stringBuilder the string builder. + * @return true if the string builder is empty, + * false otherwise. + */ + public static boolean isEmpty(@NonNull StringBuilder stringBuilder) { + return stringBuilder.length() == 0; + } + + /** + * Determines if a string builder starts with + * a specific string. + * + * @param stringBuilder the string builder. + * @param start the starting string. + * @return true if the string builder starts + * with the string, false otherwise. + */ + public static boolean startsWith(@NonNull StringBuilder stringBuilder, @NonNull String start) { + return stringBuilder.indexOf(start) == 0; + } + + /** + * Determines if a string builder contains a string. + * + * @param stringBuilder the string builder. + * @param contains the string that it might contain. + * @return true if the string builder contains the + * string, false otherwise. + */ + public static boolean contains(@NonNull StringBuilder stringBuilder, @NonNull String contains) { + return stringBuilder.indexOf(contains) >= 0; + } + + /** + * Determines equality between a string + * builder and a string. + * + * @param stringBuilder the string builder. + * @param equal the string. + * @return true if the string represented by + * the string builder is equal to the string. + */ + public static boolean equals(@NonNull StringBuilder stringBuilder, @NonNull String equal) { + + int builderLength = stringBuilder.length(); + if (builderLength != equal.length()) { + return false; + } + + for (int n = 0; n < builderLength; n++) { + if (stringBuilder.charAt(n) != equal.charAt(n)) { + return false; + } + } + + return true; + } + + /** + * Creates a sub-string builder from the + * current string builder. + * + * @param stringBuilder the string builder. + * @param start the starting index. + * @param end the ending index. + * @return a string builder that contains the + * characters between the indices. + */ + @NonNull + public static StringBuilder substring(@NonNull StringBuilder stringBuilder, int start, int end) { + StringBuilder newStringBuilder = new StringBuilder(stringBuilder); + newStringBuilder.replace(end, stringBuilder.length(), EMPTY); + newStringBuilder.replace(0, start, EMPTY); + + return newStringBuilder; + } +} diff --git a/app/src/main/java/acr/browser/lightning/utils/SubscriptionUtils.java b/app/src/main/java/acr/browser/lightning/utils/SubscriptionUtils.java new file mode 100644 index 000000000..0c6756f63 --- /dev/null +++ b/app/src/main/java/acr/browser/lightning/utils/SubscriptionUtils.java @@ -0,0 +1,26 @@ +package acr.browser.lightning.utils; + +import android.support.annotation.Nullable; + +import com.anthonycr.bonsai.Subscription; + +/** + * Utilities used when working with bonsai code. + *

+ * Created by anthonycr on 6/6/17. + */ +public final class SubscriptionUtils { + + private SubscriptionUtils() {} + + /** + * Unsubscribes from a subscription if the subscription is not null. + * + * @param subscription the subscription from which to unsubscribe. + */ + public static void safeUnsubscribe(@Nullable Subscription subscription) { + if (subscription != null) { + subscription.unsubscribe(); + } + } +} diff --git a/app/src/main/java/acr/browser/lightning/utils/ThemeUtils.java b/app/src/main/java/acr/browser/lightning/utils/ThemeUtils.java index 593fc5fa4..f28c369cc 100644 --- a/app/src/main/java/acr/browser/lightning/utils/ThemeUtils.java +++ b/app/src/main/java/acr/browser/lightning/utils/ThemeUtils.java @@ -1,5 +1,6 @@ package acr.browser.lightning.utils; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; @@ -9,7 +10,6 @@ import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.AttrRes; @@ -18,9 +18,7 @@ import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; -import android.support.v7.widget.AppCompatDrawableManager; import android.util.TypedValue; -import android.widget.ImageView; import acr.browser.lightning.R; @@ -28,18 +26,60 @@ public class ThemeUtils { private static final TypedValue sTypedValue = new TypedValue(); + /** + * Gets the primary color of the current theme. + * + * @param context the context to get the theme from. + * @return the primary color of the current theme. + */ + @ColorInt public static int getPrimaryColor(@NonNull Context context) { return getColor(context, R.attr.colorPrimary); } + /** + * Gets the primary dark color of the current theme. + * + * @param context the context to get the theme from. + * @return the primary dark color of the current theme. + */ + @ColorInt public static int getPrimaryColorDark(@NonNull Context context) { return getColor(context, R.attr.colorPrimaryDark); } + /** + * Gets the accent color of the current theme. + * + * @param context the context to get the theme from. + * @return the accent color of the current theme. + */ + @ColorInt public static int getAccentColor(@NonNull Context context) { return getColor(context, R.attr.colorAccent); } + /** + * Gets the color of the status bar as set in styles + * for the current theme. + * + * @param context the context to get the theme from. + * @return the status bar color of the current theme. + */ + @ColorInt + @TargetApi(21) + public static int getStatusBarColor(@NonNull Context context) { + return getColor(context, android.R.attr.statusBarColor); + } + + /** + * Gets the color attribute from the current theme. + * + * @param context the context to get the theme from. + * @param resource the color attribute resource. + * @return the color for the given attribute. + */ + @ColorInt public static int getColor(@NonNull Context context, @AttrRes int resource) { TypedArray a = context.obtainStyledAttributes(sTypedValue.data, new int[]{resource}); int color = a.getColor(0, 0); @@ -47,29 +87,48 @@ public static int getColor(@NonNull Context context, @AttrRes int resource) { return color; } + /** + * Gets the icon color for the light theme. + * + * @param context the context to use. + * @return the color of the icon. + */ @ColorInt public static int getIconLightThemeColor(@NonNull Context context) { return ContextCompat.getColor(context, R.color.icon_light_theme); } + /** + * Gets the icon color for the dark theme. + * + * @param context the context to use. + * @return the color of the icon. + */ @ColorInt public static int getIconDarkThemeColor(@NonNull Context context) { return ContextCompat.getColor(context, R.color.icon_dark_theme); } + /** + * Gets the color icon for the light or + * dark theme. + * + * @param context the context to use. + * @param dark true for the dark theme, + * false for the light theme. + * @return the color of the icon. + */ @ColorInt public static int getIconThemeColor(@NonNull Context context, boolean dark) { return (dark) ? getIconDarkThemeColor(context) : getIconLightThemeColor(context); } - public static void themeImageView(@NonNull ImageView icon, @NonNull Context context, boolean dark) { - int color = dark ? getIconDarkThemeColor(context) : getIconLightThemeColor(context); - icon.setColorFilter(color, PorterDuff.Mode.SRC_IN); - } - @NonNull private static Drawable getVectorDrawable(@NonNull Context context, int drawableId) { - Drawable drawable = AppCompatDrawableManager.get().getDrawable(context, drawableId); + Drawable drawable = ContextCompat.getDrawable(context, drawableId); + + Preconditions.checkNonNull(drawable); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { drawable = (DrawableCompat.wrap(drawable)).mutate(); } @@ -81,8 +140,8 @@ private static Drawable getVectorDrawable(@NonNull Context context, int drawable private static Bitmap getBitmapFromVectorDrawable(@NonNull Context context, int drawableId) { Drawable drawable = getVectorDrawable(context, drawableId); - Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), - drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), + Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); @@ -90,11 +149,23 @@ private static Bitmap getBitmapFromVectorDrawable(@NonNull Context context, int return bitmap; } + /** + * Gets the icon with an applied color filter + * for the correct theme. + * + * @param context the context to use. + * @param res the drawable resource to use. + * @param dark true for icon suitable for use with a dark theme, + * false for icon suitable for use with a light theme. + * @return a themed icon. + */ @NonNull public static Bitmap getThemedBitmap(@NonNull Context context, @DrawableRes int res, boolean dark) { int color = dark ? getIconDarkThemeColor(context) : getIconLightThemeColor(context); + Bitmap sourceBitmap = getBitmapFromVectorDrawable(context, res); - Bitmap resultBitmap = Bitmap.createBitmap(sourceBitmap.getWidth(), sourceBitmap.getHeight(), Bitmap.Config.ARGB_8888); + Bitmap resultBitmap = Bitmap.createBitmap(sourceBitmap.getWidth(), sourceBitmap.getHeight(), + Bitmap.Config.ARGB_8888); Paint p = new Paint(); ColorFilter filter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN); p.setColorFilter(filter); @@ -104,26 +175,45 @@ public static Bitmap getThemedBitmap(@NonNull Context context, @DrawableRes int return resultBitmap; } + /** + * Gets the icon with an applied color filter + * for the correct theme. + * + * @param context the context to use. + * @param res the drawable resource to use. + * @param dark true for icon suitable for use with a dark theme, + * false for icon suitable for use with a light theme. + * @return a themed icon. + */ @NonNull public static Drawable getThemedDrawable(@NonNull Context context, @DrawableRes int res, boolean dark) { int color = dark ? getIconDarkThemeColor(context) : getIconLightThemeColor(context); + final Drawable drawable = getVectorDrawable(context, res); drawable.mutate(); drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN); return drawable; } - @NonNull - public static ColorDrawable getSelectedBackground(@NonNull Context context, boolean dark) { - @ColorInt final int color = (dark) ? ContextCompat.getColor(context, R.color.selected_dark) : - ContextCompat.getColor(context, R.color.selected_light); - return new ColorDrawable(color); - } - + /** + * The text hint color for dark theme or light theme. + * + * @param dark true for a text color suitable for use with a dark theme, + * false for a text color suitable for use with a light theme. + * @return a text color. + */ + @ColorInt public static int getThemedTextHintColor(boolean dark) { return 0x80ffffff & (dark ? Color.WHITE : Color.BLACK); } + /** + * Gets the edit text text color for the current theme. + * + * @param context the context to use. + * @return a text color. + */ + @ColorInt public static int getTextColor(@NonNull Context context) { return getColor(context, android.R.attr.editTextColor); } diff --git a/app/src/main/java/acr/browser/lightning/utils/UrlUtils.java b/app/src/main/java/acr/browser/lightning/utils/UrlUtils.java index 2baa6bd97..654e41789 100644 --- a/app/src/main/java/acr/browser/lightning/utils/UrlUtils.java +++ b/app/src/main/java/acr/browser/lightning/utils/UrlUtils.java @@ -25,6 +25,7 @@ import acr.browser.lightning.constant.BookmarkPage; import acr.browser.lightning.constant.Constants; +import acr.browser.lightning.constant.DownloadsPage; import acr.browser.lightning.constant.HistoryPage; import acr.browser.lightning.constant.StartPage; @@ -33,44 +34,18 @@ */ public class UrlUtils { private static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile( - "(?i)" + // switch on case insensitive matching - '(' + // begin group for schema - "(?:http|https|file)://" + - "|(?:inline|data|about|javascript):" + - "|(?:.*:.*@)" + - ')' + - "(.*)"); + "(?i)" + // switch on case insensitive matching + '(' + // begin group for schema + "(?:http|https|file)://" + + "|(?:inline|data|about|javascript):" + + "|(?:.*:.*@)" + + ')' + + "(.*)"); // Google search public final static String QUERY_PLACE_HOLDER = "%s"; - // Regular expression to strip http:// and optionally - // the trailing slash - private static final Pattern STRIP_URL_PATTERN = - Pattern.compile("^http://(.*?)/?$"); private UrlUtils() { /* cannot be instantiated */ } - /** - * Strips the provided url of preceding "http://" and any trailing "/". Does not - * strip "https://". If the provided string cannot be stripped, the original string - * is returned. - *

- * TODO: Put this in TextUtils to be used by other packages doing something similar. - * - * @param url a url to strip, like "http://www.google.com/" - * @return a stripped url like "www.google.com", or the original string if it could - * not be stripped - */ - @Nullable - public static String stripUrl(@Nullable String url) { - if (url == null) return null; - Matcher m = STRIP_URL_PATTERN.matcher(url); - if (m.matches()) { - return m.group(1); - } else { - return url; - } - } - /** * Attempts to determine whether user input is a URL or search * terms. Anything with a space is passed to search if canBeSearch is true. @@ -106,65 +81,20 @@ public static String smartUrlFilter(@NonNull String url, boolean canBeSearch, St } if (canBeSearch) { return URLUtil.composeSearchUrl(inUrl, - searchUrl, QUERY_PLACE_HOLDER); + searchUrl, QUERY_PLACE_HOLDER); } return ""; } - /* package */ - @NonNull - static String fixUrl(@NonNull String inUrl) { - // FIXME: Converting the url to lower case - // duplicates functionality in smartUrlFilter(). - // However, changing all current callers of fixUrl to - // call smartUrlFilter in addition may have unwanted - // consequences, and is deferred for now. - int colon = inUrl.indexOf(':'); - boolean allLower = true; - for (int index = 0; index < colon; index++) { - char ch = inUrl.charAt(index); - if (!Character.isLetter(ch)) { - break; - } - allLower &= Character.isLowerCase(ch); - if (index == colon - 1 && !allLower) { - inUrl = inUrl.substring(0, colon).toLowerCase() - + inUrl.substring(colon); - } - } - if (inUrl.startsWith("http://") || inUrl.startsWith("https://")) - return inUrl; - if (inUrl.startsWith("http:") || - inUrl.startsWith("https:")) { - if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) { - inUrl = inUrl.replaceFirst("/", "//"); - } else inUrl = inUrl.replaceFirst(":", "://"); - } - return inUrl; - } - - // Returns the filtered URL. Cannot return null, but can return an empty string - /* package */ - @Nullable - static String filteredUrl(@Nullable String inUrl) { - if (inUrl == null) { - return ""; - } - if (inUrl.startsWith("content:") - || inUrl.startsWith("browser:")) { - return ""; - } - return inUrl; - } - /** * Returns whether the given url is the bookmarks/history page or a normal website */ public static boolean isSpecialUrl(@Nullable String url) { return url != null && url.startsWith(Constants.FILE) && - (url.endsWith(BookmarkPage.FILENAME) || - url.endsWith(HistoryPage.FILENAME) || - url.endsWith(StartPage.FILENAME)); + (url.endsWith(BookmarkPage.FILENAME) || + url.endsWith(DownloadsPage.FILENAME) || + url.endsWith(HistoryPage.FILENAME) || + url.endsWith(StartPage.FILENAME)); } /** @@ -177,6 +107,16 @@ public static boolean isBookmarkUrl(@Nullable String url) { return url != null && url.startsWith(Constants.FILE) && url.endsWith(BookmarkPage.FILENAME); } + /** + * Determines if the url is a url for the bookmark page. + * + * @param url the url to check, may be null. + * @return true if the url is a bookmark url, false otherwise. + */ + public static boolean isDownloadsUrl(@Nullable String url) { + return url != null && url.startsWith(Constants.FILE) && url.endsWith(DownloadsPage.FILENAME); + } + /** * Determines if the url is a url for the history page. * diff --git a/app/src/main/java/acr/browser/lightning/utils/Utils.java b/app/src/main/java/acr/browser/lightning/utils/Utils.java index bb18d6469..b626582aa 100644 --- a/app/src/main/java/acr/browser/lightning/utils/Utils.java +++ b/app/src/main/java/acr/browser/lightning/utils/Utils.java @@ -14,6 +14,7 @@ import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; @@ -33,6 +34,7 @@ import android.util.Log; import android.view.View; import android.webkit.URLUtil; +import android.widget.Toast; import com.anthonycr.grant.PermissionsManager; import com.anthonycr.grant.PermissionsResultAction; @@ -55,7 +57,7 @@ public final class Utils { - private static final String TAG = Utils.class.getSimpleName(); + private static final String TAG = "Utils"; public static boolean doesSupportHeaders() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; @@ -71,7 +73,7 @@ public static boolean doesSupportHeaders() { * @param userAgent the user agent of the browser. * @param contentDisposition the content description of the file. */ - public static void downloadFile(final Activity activity, final PreferenceManager manager, final String url, + public static void downloadFile(@NonNull final Activity activity, @NonNull final PreferenceManager manager, final String url, final String userAgent, final String contentDisposition) { PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(activity, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, new PermissionsResultAction() { @@ -79,7 +81,7 @@ public static void downloadFile(final Activity activity, final PreferenceManager public void onGranted() { String fileName = URLUtil.guessFileName(url, null, null); DownloadHandler.onDownloadStart(activity, manager, url, userAgent, contentDisposition, null); - Log.i(Constants.TAG, "Downloading: " + fileName); + Log.i(TAG, "Downloading: " + fileName); } @Override @@ -174,6 +176,18 @@ public static void showSnackbar(@NonNull Activity activity, @NonNull String mess Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show(); } + /** + * Shows a toast to the user. + * Should only be used if an activity is + * not available to show a snackbar. + * + * @param context the context needed to show the toast. + * @param resource the string shown by the toast to the user. + */ + public static void showToast(@NonNull Context context, @StringRes int resource) { + Toast.makeText(context, resource, Toast.LENGTH_SHORT).show(); + } + /** * Converts Density Pixels (DP) to Pixels (PX). * @@ -255,6 +269,7 @@ private static boolean deleteDir(@Nullable File dir) { * @param bitmap is the bitmap to pad. * @return the padded bitmap. */ + @NonNull public static Bitmap padFavicon(@NonNull Bitmap bitmap) { int padding = Utils.dpToPx(4); @@ -416,7 +431,7 @@ public static void createShortcut(@NonNull Activity activity, @NonNull HistoryIt if (TextUtils.isEmpty(item.getUrl())) { return; } - Log.d(Constants.TAG, "Creating shortcut: " + item.getTitle() + ' ' + item.getUrl()); + Log.d(TAG, "Creating shortcut: " + item.getTitle() + ' ' + item.getUrl()); Intent shortcutIntent = new Intent(activity, MainActivity.class); shortcutIntent.setData(Uri.parse(item.getUrl())); @@ -431,4 +446,36 @@ public static void createShortcut(@NonNull Activity activity, @NonNull HistoryIt Utils.showSnackbar(activity, R.string.message_added_to_homescreen); } + public static int calculateInSampleSize(@NonNull BitmapFactory.Options options, + int reqWidth, int reqHeight) { + // Raw height and width of image + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + + if (height > reqHeight || width > reqWidth) { + + final int halfHeight = height / 2; + final int halfWidth = width / 2; + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while ((halfHeight / inSampleSize) >= reqHeight + && (halfWidth / inSampleSize) >= reqWidth) { + inSampleSize *= 2; + } + } + + return inSampleSize; + } + + @Nullable + public static String guessFileExtension(@NonNull String filename) { + int lastIndex = filename.lastIndexOf('.') + 1; + if (lastIndex > 0 && filename.length() > lastIndex) { + return filename.substring(lastIndex, filename.length()); + } + return null; + } + } diff --git a/app/src/main/java/acr/browser/lightning/utils/WebUtils.java b/app/src/main/java/acr/browser/lightning/utils/WebUtils.java index 1d165d40e..2ae62c605 100644 --- a/app/src/main/java/acr/browser/lightning/utils/WebUtils.java +++ b/app/src/main/java/acr/browser/lightning/utils/WebUtils.java @@ -11,7 +11,9 @@ import android.webkit.WebView; import android.webkit.WebViewDatabase; -import acr.browser.lightning.database.HistoryDatabase; +import com.anthonycr.bonsai.Schedulers; + +import acr.browser.lightning.database.history.HistoryModel; /** * Copyright 8/4/2015 Anthony Restaino @@ -34,8 +36,10 @@ public static void clearWebStorage() { WebStorage.getInstance().deleteAllData(); } - public static void clearHistory(@NonNull Context context, @NonNull HistoryDatabase historyDatabase) { - historyDatabase.deleteHistory(); + public static void clearHistory(@NonNull Context context) { + HistoryModel.deleteHistory() + .subscribeOn(Schedulers.io()) + .subscribe(); WebViewDatabase m = WebViewDatabase.getInstance(context); m.clearFormData(); m.clearHttpAuthUsernamePassword(); diff --git a/app/src/main/java/acr/browser/lightning/view/BackgroundDrawable.java b/app/src/main/java/acr/browser/lightning/view/BackgroundDrawable.java index 94da56bd9..ec1ad8523 100644 --- a/app/src/main/java/acr/browser/lightning/view/BackgroundDrawable.java +++ b/app/src/main/java/acr/browser/lightning/view/BackgroundDrawable.java @@ -4,6 +4,7 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; +import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import acr.browser.lightning.R; @@ -17,7 +18,7 @@ public class BackgroundDrawable extends TransitionDrawable { * Create a new transition drawable with the specified list of layers. At least * 2 layers are required for this drawable to work properly. */ - public BackgroundDrawable(Context context) { + public BackgroundDrawable(@NonNull Context context) { super(new Drawable[]{new ColorDrawable(ContextCompat.getColor(context, R.color.transparent)), new ColorDrawable(ThemeUtils.getColor(context, R.attr.selectedBackground))}); } diff --git a/app/src/main/java/acr/browser/lightning/view/IconCacheTask.java b/app/src/main/java/acr/browser/lightning/view/IconCacheTask.java deleted file mode 100644 index ec4018860..000000000 --- a/app/src/main/java/acr/browser/lightning/view/IconCacheTask.java +++ /dev/null @@ -1,42 +0,0 @@ -package acr.browser.lightning.view; - -import android.app.Application; -import android.graphics.Bitmap; -import android.net.Uri; -import android.util.Log; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; - -import acr.browser.lightning.constant.Constants; -import acr.browser.lightning.utils.Utils; - -class IconCacheTask implements Runnable { - private final Uri uri; - private final Bitmap icon; - private final Application app; - - public IconCacheTask(final Uri uri, final Bitmap icon, final Application app) { - this.uri = uri; - this.icon = icon; - this.app = app; - } - - @Override - public void run() { - String hash = String.valueOf(uri.getHost().hashCode()); - Log.d(Constants.TAG, "Caching icon for " + uri.getHost()); - FileOutputStream fos = null; - try { - File image = new File(app.getCacheDir(), hash + ".png"); - fos = new FileOutputStream(image); - icon.compress(Bitmap.CompressFormat.PNG, 100, fos); - fos.flush(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - Utils.close(fos); - } - } -} diff --git a/app/src/main/java/acr/browser/lightning/view/LightningChromeClient.java b/app/src/main/java/acr/browser/lightning/view/LightningChromeClient.java index d28cdde47..80f40bc06 100644 --- a/app/src/main/java/acr/browser/lightning/view/LightningChromeClient.java +++ b/app/src/main/java/acr/browser/lightning/view/LightningChromeClient.java @@ -2,7 +2,6 @@ import android.Manifest; import android.app.Activity; -import android.content.Context; import android.content.DialogInterface; import android.content.res.Resources; import android.graphics.Bitmap; @@ -19,28 +18,32 @@ import android.webkit.WebChromeClient; import android.webkit.WebView; +import com.anthonycr.bonsai.Schedulers; import com.anthonycr.grant.PermissionsManager; import com.anthonycr.grant.PermissionsResultAction; +import javax.inject.Inject; + import acr.browser.lightning.R; import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.controller.UIController; import acr.browser.lightning.dialog.BrowserDialog; +import acr.browser.lightning.favicon.FaviconModel; import acr.browser.lightning.utils.Preconditions; -class LightningChromeClient extends WebChromeClient { - - private static final String TAG = LightningChromeClient.class.getSimpleName(); +public class LightningChromeClient extends WebChromeClient { private static final String[] PERMISSIONS = new String[]{Manifest.permission.ACCESS_FINE_LOCATION}; @NonNull private final Activity mActivity; @NonNull private final LightningView mLightningView; @NonNull private final UIController mUIController; + @Inject FaviconModel mFaviconModel; LightningChromeClient(@NonNull Activity activity, @NonNull LightningView lightningView) { Preconditions.checkNonNull(activity); Preconditions.checkNonNull(lightningView); + BrowserApp.getAppComponent().inject(this); mActivity = activity; mUIController = (UIController) activity; mLightningView = lightningView; @@ -57,7 +60,7 @@ public void onProgressChanged(WebView view, int newProgress) { public void onReceivedIcon(@NonNull WebView view, Bitmap icon) { mLightningView.getTitleInfo().setFavicon(icon); mUIController.tabChanged(mLightningView); - cacheFavicon(view.getUrl(), icon, mActivity); + cacheFavicon(view.getUrl(), icon); } /** @@ -65,13 +68,20 @@ public void onReceivedIcon(@NonNull WebView view, Bitmap icon) { * * @param icon the icon to cache */ - private static void cacheFavicon(@Nullable final String url, @Nullable final Bitmap icon, @NonNull final Context context) { - if (icon == null || url == null) return; - final Uri uri = Uri.parse(url); + private void cacheFavicon(@Nullable final String url, @Nullable final Bitmap icon) { + if (icon == null || url == null) { + return; + } + + Uri uri = Uri.parse(url); + if (uri.getHost() == null) { return; } - BrowserApp.getIOThread().execute(new IconCacheTask(uri, icon, BrowserApp.get(context))); + + mFaviconModel.cacheFaviconForUrl(icon, url) + .subscribeOn(Schedulers.io()) + .subscribe(); } diff --git a/app/src/main/java/acr/browser/lightning/view/LightningView.java b/app/src/main/java/acr/browser/lightning/view/LightningView.java index e7b0785d4..8532deae4 100644 --- a/app/src/main/java/acr/browser/lightning/view/LightningView.java +++ b/app/src/main/java/acr/browser/lightning/view/LightningView.java @@ -32,7 +32,11 @@ import android.webkit.WebSettings.PluginState; import android.webkit.WebView; -import com.squareup.otto.Bus; +import com.anthonycr.bonsai.Schedulers; +import com.anthonycr.bonsai.Single; +import com.anthonycr.bonsai.SingleAction; +import com.anthonycr.bonsai.SingleOnSubscribe; +import com.anthonycr.bonsai.SingleSubscriber; import java.io.File; import java.lang.ref.WeakReference; @@ -43,20 +47,14 @@ import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.constant.BookmarkPage; import acr.browser.lightning.constant.Constants; +import acr.browser.lightning.constant.DownloadsPage; import acr.browser.lightning.constant.HistoryPage; import acr.browser.lightning.constant.StartPage; import acr.browser.lightning.controller.UIController; -import acr.browser.lightning.database.BookmarkManager; import acr.browser.lightning.dialog.LightningDialogBuilder; import acr.browser.lightning.download.LightningDownloadListener; import acr.browser.lightning.preference.PreferenceManager; - -import com.anthonycr.bonsai.Action; -import com.anthonycr.bonsai.Observable; -import com.anthonycr.bonsai.Schedulers; -import com.anthonycr.bonsai.Subscriber; -import com.anthonycr.bonsai.OnSubscribe; - +import acr.browser.lightning.utils.Preconditions; import acr.browser.lightning.utils.ProxyUtils; import acr.browser.lightning.utils.UrlUtils; import acr.browser.lightning.utils.Utils; @@ -69,7 +67,7 @@ */ public class LightningView { - private static final String TAG = LightningView.class.getSimpleName(); + private static final String TAG = "LightningView"; public static final String HEADER_REQUESTED_WITH = "X-Requested-With"; public static final String HEADER_WAP_PROFILE = "X-Wap-Profile"; @@ -77,8 +75,8 @@ public class LightningView { private static final int API = android.os.Build.VERSION.SDK_INT; private static final int SCROLL_UP_THRESHOLD = Utils.dpToPx(10); - private static String sHomepage; - private static String sDefaultUserAgent; + @Nullable private static String sHomepage; + @Nullable private static String sDefaultUserAgent; private static float sMaxFling; private static final float[] sNegativeColorArray = { -1.0f, 0, 0, 0, 255, // red @@ -99,19 +97,17 @@ public class LightningView { @NonNull private final GestureDetector mGestureDetector; @NonNull private final Activity mActivity; @NonNull private final Paint mPaint = new Paint(); - @Nullable private Object mTag; + private boolean mIsNewTab; private final boolean mIsIncognitoTab; - private boolean isForegroundTab; + private boolean mIsForegroundTab; private boolean mInvertPage = false; private boolean mToggleDesktop = false; @NonNull private final WebViewHandler mWebViewHandler = new WebViewHandler(this); @NonNull private final Map mRequestHeaders = new ArrayMap<>(); - @Inject Bus mEventBus; @Inject PreferenceManager mPreferences; @Inject LightningDialogBuilder mBookmarksDialogBuilder; @Inject ProxyUtils mProxyUtils; - @Inject BookmarkManager mBookmarkManager; public LightningView(@NonNull Activity activity, @Nullable String url, boolean isIncognito) { BrowserApp.getAppComponent().inject(this); @@ -163,25 +159,27 @@ public LightningView(@NonNull Activity activity, @Nullable String url, boolean i } /** - * Sets the tag on the object, - * a reference to this object is held - * indefinitely. + * Sets whether this tab was the + * result of a new intent sent + * to the browser. * - * @param tag the tag to set, may be null. + * @param isNewTab true if it's from + * a new intent, + * false otherwise. */ - public void setTag(@Nullable Object tag) { - mTag = tag; + public void setIsNewTab(boolean isNewTab) { + mIsNewTab = isNewTab; } /** - * The tag set on the object. + * Returns whether this tab was created + * as a result of a new intent. * - * @return the tag set on the object, - * may be null. + * @return true if it was a new intent, + * false otherwise. */ - @Nullable - public Object getTag() { - return mTag; + public boolean isNewTab() { + return mIsNewTab; } /** @@ -194,6 +192,8 @@ public void loadHomepage() { if (mWebView == null) { return; } + + Preconditions.checkNonNull(sHomepage); switch (sHomepage) { case Constants.SCHEME_HOMEPAGE: loadStartpage(); @@ -213,7 +213,16 @@ public void loadHomepage() { * UI thread. */ private void loadStartpage() { - new StartPage(this, BrowserApp.get(mActivity)).load(); + new StartPage().getHomepage() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable String item) { + Preconditions.checkNonNull(item); + loadUrl(item); + } + }); } /** @@ -222,9 +231,34 @@ private void loadStartpage() { * UI thread. It also caches the default folder icon locally. */ public void loadBookmarkpage() { - if (mWebView == null) - return; - new BookmarkPage(this, mActivity, mBookmarkManager).load(); + new BookmarkPage(mActivity).getBookmarkPage() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable String item) { + Preconditions.checkNonNull(item); + loadUrl(item); + } + }); + } + + /** + * This method gets the bookmark page URL from the {@link BookmarkPage} + * class asynchronously and loads the URL in the WebView on the + * UI thread. It also caches the default folder icon locally. + */ + public void loadDownloadspage() { + new DownloadsPage().getDownloadsPage() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.main()) + .subscribe(new SingleOnSubscribe() { + @Override + public void onItem(@Nullable String item) { + Preconditions.checkNonNull(item); + loadUrl(item); + } + }); } /** @@ -405,59 +439,55 @@ private void initializeSettings() { settings.setAllowUniversalAccessFromFileURLs(false); } - getPathObservable("appcache") - .subscribeOn(Schedulers.io()) + getPathObservable("appcache").subscribeOn(Schedulers.io()) .observeOn(Schedulers.main()) - .subscribe(new OnSubscribe() { + .subscribe(new SingleOnSubscribe() { @Override - public void onNext(File item) { + public void onItem(@Nullable File item) { + Preconditions.checkNonNull(item); settings.setAppCachePath(item.getPath()); } - - @Override - public void onComplete() {} }); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - getPathObservable("geolocation") - .subscribeOn(Schedulers.io()) + getPathObservable("geolocation").subscribeOn(Schedulers.io()) .observeOn(Schedulers.main()) - .subscribe(new OnSubscribe() { + .subscribe(new SingleOnSubscribe() { @Override - public void onNext(File item) { + public void onItem(@Nullable File item) { + Preconditions.checkNonNull(item); //noinspection deprecation settings.setGeolocationDatabasePath(item.getPath()); } - - @Override - public void onComplete() {} }); } - getPathObservable("databases") - .subscribeOn(Schedulers.io()) + getPathObservable("databases").subscribeOn(Schedulers.io()) .observeOn(Schedulers.main()) - .subscribe(new OnSubscribe() { + .subscribe(new SingleOnSubscribe() { @Override - public void onNext(File item) { + public void onItem(@Nullable File item) { if (API < Build.VERSION_CODES.KITKAT) { + Preconditions.checkNonNull(item); //noinspection deprecation settings.setDatabasePath(item.getPath()); } } @Override - public void onComplete() {} + public void onComplete() { + } }); } - private Observable getPathObservable(final String subFolder) { - return Observable.create(new Action() { + @NonNull + private Single getPathObservable(final String subFolder) { + return Single.create(new SingleAction() { @Override - public void onSubscribe(@NonNull Subscriber subscriber) { - File file = BrowserApp.get(mActivity).getDir(subFolder, 0); - subscriber.onNext(file); + public void onSubscribe(@NonNull SingleSubscriber subscriber) { + File file = mActivity.getDir(subFolder, 0); + subscriber.onItem(file); subscriber.onComplete(); } }); @@ -604,8 +634,8 @@ public synchronized void freeMemory() { * @param isForeground true if the tab should be set as * foreground, false otherwise. */ - public void setForegroundTab(boolean isForeground) { - isForegroundTab = isForeground; + public void setIsForegroundTab(boolean isForeground) { + mIsForegroundTab = isForeground; mUIController.tabChanged(this); } @@ -616,7 +646,7 @@ public void setForegroundTab(boolean isForeground) { * false otherwise. */ public boolean isForegroundTab() { - return isForegroundTab; + return mIsForegroundTab; } /** @@ -961,36 +991,43 @@ private void longClickPage(@Nullable final String url) { if (currentUrl != null && UrlUtils.isSpecialUrl(currentUrl)) { if (currentUrl.endsWith(HistoryPage.FILENAME)) { if (url != null) { - mBookmarksDialogBuilder.showLongPressedHistoryLinkDialog(mActivity, url); + mBookmarksDialogBuilder.showLongPressedHistoryLinkDialog(mActivity, mUIController, url); } else if (result != null && result.getExtra() != null) { final String newUrl = result.getExtra(); - mBookmarksDialogBuilder.showLongPressedHistoryLinkDialog(mActivity, newUrl); + mBookmarksDialogBuilder.showLongPressedHistoryLinkDialog(mActivity, mUIController, newUrl); } } else if (currentUrl.endsWith(BookmarkPage.FILENAME)) { if (url != null) { - mBookmarksDialogBuilder.showLongPressedDialogForBookmarkUrl(mActivity, url); + mBookmarksDialogBuilder.showLongPressedDialogForBookmarkUrl(mActivity, mUIController, url); + } else if (result != null && result.getExtra() != null) { + final String newUrl = result.getExtra(); + mBookmarksDialogBuilder.showLongPressedDialogForBookmarkUrl(mActivity, mUIController, newUrl); + } + } else if (currentUrl.endsWith(DownloadsPage.FILENAME)) { + if (url != null) { + mBookmarksDialogBuilder.showLongPressedDialogForDownloadUrl(mActivity, mUIController, url); } else if (result != null && result.getExtra() != null) { final String newUrl = result.getExtra(); - mBookmarksDialogBuilder.showLongPressedDialogForBookmarkUrl(mActivity, newUrl); + mBookmarksDialogBuilder.showLongPressedDialogForDownloadUrl(mActivity, mUIController, newUrl); } } } else { if (url != null) { if (result != null) { if (result.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE || result.getType() == WebView.HitTestResult.IMAGE_TYPE) { - mBookmarksDialogBuilder.showLongPressImageDialog(mActivity, url, getUserAgent()); + mBookmarksDialogBuilder.showLongPressImageDialog(mActivity, mUIController, url, getUserAgent()); } else { - mBookmarksDialogBuilder.showLongPressLinkDialog(mActivity, url); + mBookmarksDialogBuilder.showLongPressLinkDialog(mActivity, mUIController, url); } } else { - mBookmarksDialogBuilder.showLongPressLinkDialog(mActivity, url); + mBookmarksDialogBuilder.showLongPressLinkDialog(mActivity, mUIController, url); } } else if (result != null && result.getExtra() != null) { final String newUrl = result.getExtra(); if (result.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE || result.getType() == WebView.HitTestResult.IMAGE_TYPE) { - mBookmarksDialogBuilder.showLongPressImageDialog(mActivity, newUrl, getUserAgent()); + mBookmarksDialogBuilder.showLongPressImageDialog(mActivity, mUIController, newUrl, getUserAgent()); } else { - mBookmarksDialogBuilder.showLongPressLinkDialog(mActivity, newUrl); + mBookmarksDialogBuilder.showLongPressLinkDialog(mActivity, mUIController, newUrl); } } } @@ -1198,7 +1235,7 @@ private static class WebViewHandler extends Handler { @NonNull private final WeakReference mReference; - public WebViewHandler(LightningView view) { + WebViewHandler(@NonNull LightningView view) { mReference = new WeakReference<>(view); } diff --git a/app/src/main/java/acr/browser/lightning/view/LightningWebClient.java b/app/src/main/java/acr/browser/lightning/view/LightningWebClient.java index e5f24f88f..8e3f4bb25 100644 --- a/app/src/main/java/acr/browser/lightning/view/LightningWebClient.java +++ b/app/src/main/java/acr/browser/lightning/view/LightningWebClient.java @@ -8,16 +8,19 @@ import android.content.Intent; import android.graphics.Bitmap; import android.net.MailTo; +import android.net.Uri; import android.net.http.SslError; import android.os.Build; import android.os.Message; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.content.FileProvider; import android.support.v7.app.AlertDialog; -import android.text.InputType; -import android.text.method.PasswordTransformationMethod; import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; import android.webkit.HttpAuthHandler; +import android.webkit.MimeTypeMap; import android.webkit.SslErrorHandler; import android.webkit.ValueCallback; import android.webkit.WebResourceRequest; @@ -25,9 +28,10 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.EditText; -import android.widget.LinearLayout; +import android.widget.TextView; import java.io.ByteArrayInputStream; +import java.io.File; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; @@ -35,6 +39,7 @@ import javax.inject.Inject; +import acr.browser.lightning.BuildConfig; import acr.browser.lightning.R; import acr.browser.lightning.app.BrowserApp; import acr.browser.lightning.constant.Constants; @@ -48,6 +53,8 @@ public class LightningWebClient extends WebViewClient { + private static final String TAG = "LightningWebClient"; + @NonNull private final Activity mActivity; @NonNull private final LightningView mLightningView; @NonNull private final UIController mUIController; @@ -125,44 +132,38 @@ public void onReceivedHttpAuthRequest(final WebView view, @NonNull final HttpAut final String host, final String realm) { AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); - final EditText name = new EditText(mActivity); - final EditText password = new EditText(mActivity); - LinearLayout passLayout = new LinearLayout(mActivity); - passLayout.setOrientation(LinearLayout.VERTICAL); - - passLayout.addView(name); - passLayout.addView(password); - - name.setHint(mActivity.getString(R.string.hint_username)); - name.setSingleLine(); - password.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD); - password.setSingleLine(); - password.setTransformationMethod(new PasswordTransformationMethod()); - password.setHint(mActivity.getString(R.string.hint_password)); - builder.setTitle(mActivity.getString(R.string.title_sign_in)); - builder.setView(passLayout); - builder.setCancelable(true) - .setPositiveButton(mActivity.getString(R.string.title_sign_in), + + View dialogView = LayoutInflater.from(mActivity).inflate(R.layout.dialog_auth_request, null); + + final TextView realmLabel = (TextView) dialogView.findViewById(R.id.auth_request_realm_textview); + final EditText name = (EditText) dialogView.findViewById(R.id.auth_request_username_edittext); + final EditText password = (EditText) dialogView.findViewById(R.id.auth_request_password_edittext); + + realmLabel.setText(mActivity.getString(R.string.label_realm, realm)); + + builder.setView(dialogView) + .setTitle(R.string.title_sign_in) + .setCancelable(true) + .setPositiveButton(R.string.title_sign_in, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { String user = name.getText().toString(); String pass = password.getText().toString(); handler.proceed(user.trim(), pass.trim()); - Log.d(Constants.TAG, "Request Login"); - + Log.d(TAG, "Attempting HTTP Authentication"); } }) - .setNegativeButton(mActivity.getString(R.string.action_cancel), + .setNegativeButton(R.string.action_cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { handler.cancel(); } }); - AlertDialog alert = builder.create(); - alert.show(); - BrowserDialog.setDialogSize(mActivity, alert); + AlertDialog dialog = builder.create(); + dialog.show(); + BrowserDialog.setDialogSize(mActivity, dialog); } private volatile boolean mIsRunning = false; @@ -280,7 +281,7 @@ public void onClick(DialogInterface dialog, int id) { @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull WebResourceRequest request) { return shouldOverrideLoading(view, request.getUrl().toString()) || super.shouldOverrideUrlLoading(view, request); } @@ -290,7 +291,7 @@ public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull String u return shouldOverrideLoading(view, url) || super.shouldOverrideUrlLoading(view, url); } - private boolean shouldOverrideLoading(WebView view, String url) { + private boolean shouldOverrideLoading(@NonNull WebView view, @NonNull String url) { // Check if configured proxy is available if (!mProxyUtils.isProxyReady(mActivity)) { // User has been notified @@ -299,45 +300,35 @@ private boolean shouldOverrideLoading(WebView view, String url) { Map headers = mLightningView.getRequestHeaders(); - // If the headers are empty, the user has not expressed the desire - // to use them and therefore we can revert back to the old way of loading - if (headers.isEmpty()) { - if (mLightningView.isIncognito()) { - // If we are in incognito, immediately load, we don't want the url to leave the app - return false; - } - if (url.startsWith(Constants.ABOUT)) { - // If this is an about page, immediately load, we don't need to leave the app - return false; - } + if (mLightningView.isIncognito()) { + // If we are in incognito, immediately load, we don't want the url to leave the app + return continueLoadingUrl(view, url, headers); + } + if (url.startsWith(Constants.ABOUT)) { + // If this is an about page, immediately load, we don't need to leave the app + return continueLoadingUrl(view, url, headers); + } - if (isMailOrIntent(url, view) || mIntentUtils.startActivityForUrl(view, url)) { - // If it was a mailto: link, or an intent, or could be launched elsewhere, do that - return true; - } - } else { - if (mLightningView.isIncognito() && Utils.doesSupportHeaders()) { - // If we are in incognito, immediately load, we don't want the url to leave the app - view.loadUrl(url, headers); - return true; - } - if (url.startsWith(Constants.ABOUT) && Utils.doesSupportHeaders()) { - // If this is an about page, immediately load, we don't need to leave the app - view.loadUrl(url, headers); - return true; - } + if (isMailOrIntent(url, view) || mIntentUtils.startActivityForUrl(view, url)) { + // If it was a mailto: link, or an intent, or could be launched elsewhere, do that + return true; + } - if (isMailOrIntent(url, view) || mIntentUtils.startActivityForUrl(view, url)) { - // If it was a mailto: link, or an intent, or could be launched elsewhere, do that - return true; - } else if (Utils.doesSupportHeaders()) { - // Otherwise, load the headers. - view.loadUrl(url, headers); - return true; - } + // If none of the special conditions was met, continue with loading the url + return continueLoadingUrl(view, url, headers); + } + + private boolean continueLoadingUrl(@NonNull WebView webView, + @NonNull String url, + @NonNull Map headers) { + if (headers.isEmpty()) { + return false; + } else if (Utils.doesSupportHeaders()) { + webView.loadUrl(url, headers); + return true; + } else { + return false; } - // If none of those instances was true, revert back to the old way of loading - return false; } private boolean isMailOrIntent(@NonNull String url, @NonNull WebView view) { @@ -364,7 +355,27 @@ private boolean isMailOrIntent(@NonNull String url, @NonNull WebView view) { try { mActivity.startActivity(intent); } catch (ActivityNotFoundException e) { - Log.e(Constants.TAG, "ActivityNotFoundException"); + Log.e(TAG, "ActivityNotFoundException"); + } + return true; + } + } else if (url.startsWith(Constants.FILE)) { + File file = new File(url.replace(Constants.FILE, "")); + + if (file.exists()) { + String newMimeType = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(Utils.guessFileExtension(file.toString())); + + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + Uri contentUri = FileProvider.getUriForFile(mActivity, BuildConfig.APPLICATION_ID + ".fileprovider", file); + intent.setDataAndType(contentUri, newMimeType); + + try { + mActivity.startActivity(intent); + } catch (Exception e) { + System.out.println("LightningWebClient: cannot open downloaded file"); } return true; } diff --git a/app/src/main/java/acr/browser/lightning/view/SearchView.java b/app/src/main/java/acr/browser/lightning/view/SearchView.java index 4551c051e..80b6c9aec 100644 --- a/app/src/main/java/acr/browser/lightning/view/SearchView.java +++ b/app/src/main/java/acr/browser/lightning/view/SearchView.java @@ -1,13 +1,14 @@ package acr.browser.lightning.view; import android.content.Context; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v7.widget.AppCompatAutoCompleteTextView; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.ViewConfiguration; -import android.widget.AutoCompleteTextView; -public class SearchView extends AutoCompleteTextView { +public class SearchView extends AppCompatAutoCompleteTextView { public interface PreFocusListener { void onPreFocus(); @@ -17,15 +18,15 @@ public interface PreFocusListener { private boolean mIsBeingClicked; private long mTimePressed; - public SearchView(Context context) { + public SearchView(@NonNull Context context) { super(context); } - public SearchView(Context context, AttributeSet attrs) { + public SearchView(@NonNull Context context, AttributeSet attrs) { super(context, attrs); } - public SearchView(Context context, AttributeSet attrs, int defStyleAttr) { + public SearchView(@NonNull Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @@ -34,7 +35,7 @@ public void setOnPreFocusListener(@Nullable PreFocusListener listener) { } @Override - public boolean onTouchEvent(MotionEvent event) { + public boolean onTouchEvent(@NonNull MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mTimePressed = System.currentTimeMillis(); diff --git a/app/src/main/res/drawable/tab_vertical_background.xml b/app/src/main/res/drawable/tab_vertical_background.xml deleted file mode 100644 index 6286c4e79..000000000 --- a/app/src/main/res/drawable/tab_vertical_background.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/animated_progress_bar.xml b/app/src/main/res/layout/animated_progress_bar.xml deleted file mode 100644 index 7bb4ad110..000000000 --- a/app/src/main/res/layout/animated_progress_bar.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/bookmark_drawer.xml b/app/src/main/res/layout/bookmark_drawer.xml index 2345bcd6d..265a23d7e 100644 --- a/app/src/main/res/layout/bookmark_drawer.xml +++ b/app/src/main/res/layout/bookmark_drawer.xml @@ -43,14 +43,13 @@ android:textAppearance="?android:attr/textAppearanceLarge"/> - + android:dividerHeight="0dp"/> - + android:orientation="horizontal"> + android:gravity="center_vertical"> + android:textAppearance="?android:attr/textAppearanceListItemSmall"/> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_auth_request.xml b/app/src/main/res/layout/dialog_auth_request.xml new file mode 100644 index 000000000..d82335a6c --- /dev/null +++ b/app/src/main/res/layout/dialog_auth_request.xml @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_edit_bookmark.xml b/app/src/main/res/layout/dialog_edit_bookmark.xml index 7141e0a69..19db5d239 100644 --- a/app/src/main/res/layout/dialog_edit_bookmark.xml +++ b/app/src/main/res/layout/dialog_edit_bookmark.xml @@ -1,29 +1,33 @@ - + + android:hint="@string/hint_title" + android:maxLines="1" + android:singleLine="true"/> + android:inputType="textUri" + android:maxLines="1" + android:singleLine="true"/> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_edit_text.xml b/app/src/main/res/layout/dialog_edit_text.xml new file mode 100644 index 000000000..283c5a806 --- /dev/null +++ b/app/src/main/res/layout/dialog_edit_text.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/picker_manual_proxy.xml b/app/src/main/res/layout/dialog_manual_proxy.xml similarity index 70% rename from app/src/main/res/layout/picker_manual_proxy.xml rename to app/src/main/res/layout/dialog_manual_proxy.xml index c59d030f4..cb7d32a9f 100644 --- a/app/src/main/res/layout/picker_manual_proxy.xml +++ b/app/src/main/res/layout/dialog_manual_proxy.xml @@ -1,12 +1,10 @@ - + android:padding="@dimen/dialog_padding"> + android:layout_marginRight="@dimen/default_padding" + android:text="@string/host"/> + android:inputType="text"/> + android:layout_marginRight="@dimen/default_padding" + android:text="@string/port"/> + android:inputType="number"/> \ No newline at end of file diff --git a/app/src/main/res/layout/seek_layout.xml b/app/src/main/res/layout/dialog_seek_bar.xml similarity index 50% rename from app/src/main/res/layout/seek_layout.xml rename to app/src/main/res/layout/dialog_seek_bar.xml index 0616d8d1a..eb62327d7 100644 --- a/app/src/main/res/layout/seek_layout.xml +++ b/app/src/main/res/layout/dialog_seek_bar.xml @@ -1,13 +1,14 @@ - + android:orientation="vertical" + android:padding="@dimen/dialog_padding"> + android:layout_height="wrap_content"/> \ No newline at end of file diff --git a/app/src/main/res/layout/tab_list_item.xml b/app/src/main/res/layout/tab_list_item.xml index 6cbb5e9d1..dcbb69983 100644 --- a/app/src/main/res/layout/tab_list_item.xml +++ b/app/src/main/res/layout/tab_list_item.xml @@ -48,7 +48,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:contentDescription="Delete Tab" + android:contentDescription="@string/close_tab" app:srcCompat="@drawable/ic_action_delete"/> \ No newline at end of file diff --git a/app/src/main/res/layout/tab_list_item_horizontal.xml b/app/src/main/res/layout/tab_list_item_horizontal.xml index 1651425d6..545e3a51e 100644 --- a/app/src/main/res/layout/tab_list_item_horizontal.xml +++ b/app/src/main/res/layout/tab_list_item_horizontal.xml @@ -47,7 +47,7 @@ android:layout_width="20dp" android:layout_height="20dp" android:layout_gravity="center" - android:contentDescription="Delete Tab" + android:contentDescription="@string/close_tab" app:srcCompat="@drawable/ic_action_delete"/> diff --git a/app/src/main/res/menu-large/main.xml b/app/src/main/res/menu-large/main.xml index 92b63e110..958acfaa1 100644 --- a/app/src/main/res/menu-large/main.xml +++ b/app/src/main/res/menu-large/main.xml @@ -47,6 +47,9 @@ + diff --git a/app/src/main/res/menu-xlarge/main.xml b/app/src/main/res/menu-xlarge/main.xml index 92b63e110..958acfaa1 100644 --- a/app/src/main/res/menu-xlarge/main.xml +++ b/app/src/main/res/menu-xlarge/main.xml @@ -47,6 +47,9 @@ + diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml index 9bfa01bf0..f620979fd 100644 --- a/app/src/main/res/menu/main.xml +++ b/app/src/main/res/menu/main.xml @@ -29,6 +29,9 @@ + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 06c5eaa59..7124a82ad 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -45,7 +45,7 @@ مخصص محرك البحث حسنا - هل ترغب في تحميل هذا الملف؟ + هل ترغب في تحميل هذا الملف؟ %1$s إلغاء تحذير لم يتم العثور على مشغل الفلاش يرجى تثبيته من المتجر. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 57ce8fb11..a6b6bf320 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -69,7 +69,7 @@ Andere Suchmaschine Ok - Möchten Sie diese Datei herunterladen? + Möchten Sie diese Datei herunterladen? (%1$s) Abbrechen Warnung Adobe Flash Player wurde nicht erkannt.\nBitte installieren Sie den Flash Player. @@ -79,8 +79,8 @@ Webseite Verlauf löschen Cookies löschen - Möchten Sie den gesamten Verlauf sicher löschen? - Möchten Sie alle Cookies sicher löschen? + Möchten Sie den gesamten Verlauf löschen? + Möchten Sie alle Cookies löschen? Ja Nein Textgröße @@ -101,10 +101,10 @@ Auf Seite suchen Herunterladen läuft\u2026 Herunterladen nur bei \"http\" oder \"https\" URLs möglich! - Keine SD-Karte - USB-Speicher ist für das Herunterladen der Datei notwendig! - USB-Speicher nicht verfügbar! - Speicher aktiv. Um ein Herunterladen zu ermöglichen, schalten Sie den USB-Speicher in der Benachrichtigung aus. + Keine SD-Karte + USB-Speicher ist für das Herunterladen der Datei notwendig! + USB-Speicher nicht verfügbar + Speicher aktiv. Um ein Herunterladen zu ermöglichen, schalten Sie den USB-Speicher in der Benachrichtigung aus. Cookies im Inkognito-Modus aktivieren Adobe Flash Manuell @@ -120,7 +120,7 @@ Text in Zwischenablage kopiert Link in Zwischenablage kopiert Benutzerdefinierte URL - Lokale Datei wurde beim Laden blockiert! + Lokale Datei wurde beim Laden blockiert Open Source-Lizenzen Suche nach Werbung blockieren @@ -157,18 +157,18 @@ Dateiauswahl NetCipher GNU Lesser General Public License - Exportiere Backup - Importiere Backup + Exportiere Datensicherung + Importiere Lesezeichen aus Datensicherung Lesezeichen exportiert nach - Lesezeichen - Lesezeichen konnten nicht imprortiert werden + Lesezeichen Einstellungen + Lesezeichen konnten nicht importiert werden Wähle Datei aus Allgemein - Display Privatsphäre Über Details über Version, Autor und Lizenz - Tab schließen + Andere Tabs schließen + Aktuellen Tab schließen Alle Tabs schließen Drittanbieter Cookies blockieren Farbmodus aktivieren @@ -180,9 +180,9 @@ MIT Lizenz URL-Box Inhalt - Domain (Standard) - URL - Titel + Domain (Standard) + URL + Titel Ordner Umbenennen @@ -202,6 +202,8 @@ Tabs App-Design Ordner umbenennen + Auf Startbildschirm legen + Verknüpfung auf dem Startbildschirm abgelegt HTTP-Proxy I2P läuft nicht. I2P Tunnel sind noch nicht bereit. @@ -216,4 +218,32 @@ Es sieht aus, als ob I2P installiert wäre. Möchten Sie es verwenden? Textcodierung Tabs in Drawer anzeigen + Alle Lesezeichen löschen + Bitte schließen Sie die App um die Änderungen anzuwenden. + Ordner löschen + Lesezeichen löschen + In neuem Tab öffnen + In Inkognito-Tab öffnen + Lesezeichen bearbeiten + Debug Einstellungen + Link kopieren + Bild herunterladen + In Hintergrund-Tab öffnen + Aus dem Verlauf entfernen + Ordner umbenennen + Browser schließen + \"Nicht verfolgen\"-Funktion anfordern + FAQ + Häufige Fragen + Kontrast erhöhen + Unterstützt durch DuckDuckGo + Identifizierbare Header entfernen + Keine Suchvorschläge + Werbeblocker + Unterstützte Browser + Lesezeichen und Tab-Drawer wechseln + Standardbrowser + Host: + Quelle für Werbeblocker + Anzeige diff --git a/app/src/main/res/values-gr/strings.xml b/app/src/main/res/values-el/strings.xml similarity index 99% rename from app/src/main/res/values-gr/strings.xml rename to app/src/main/res/values-el/strings.xml index 8e8e7298f..42e1d345e 100644 --- a/app/src/main/res/values-gr/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -53,7 +53,7 @@ Συγκεκριμένο Μηχανή Αναζήτησης OK - Θα θέλατε να κατεβάσετε αυτό το αρχείο; + Θα θέλατε να κατεβάσετε αυτό το αρχείο; (%1$s) Ακύρωση Προσοχή Δεν ανιχνεύτηκε ο Adobe Flash Player.\nΠαρακαλώ εγκαταστήστε τον Flash Player. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 876d8ed6e..581cf5cfb 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,5 +1,4 @@ - Lightning Nueva pestaña Compartir @@ -9,19 +8,19 @@ Copiar enlace Adelante Ajustes - Acceder a tu ubicación - Almacenar contraseñas + Permitir acceso a tu ubicación + Almacenar tus contraseñas Agente de usuario Habilitar Adobe Flash Página de inicio - Habilitar pantalla completa + Modo de pantalla completa Habilitar JavaScript Ruta de descargas Ajustes avanzados - Apache License 2.0 + Licencia Apache 2.0 Versión Vaciar caché al salir - Activar el Text Reflow + Activar la recolocación dinámica de texto Bloquear imágenes Permitir a los sitios abrir nuevas ventanas Habilitar cookies @@ -33,8 +32,10 @@ Utilizar viewport normal Visualizar la página de lejos al cargar Restaurar pestañas cerradas al iniciar - (No se detecta navegador de sistema) - (Detectado navegador de sistema soportado) + Navegadores compatibles + Navegador de sistema + Navegador de sistema no detectado + Detectado navegador de sistema soportado Ocultar barra de estado al navegar Limpiar cookies Limpiar historial @@ -52,12 +53,12 @@ Personalizado Motor de búsqueda Aceptar - ¿Quieres descargar este archivo? + ¿Quieres descargar este archivo? (%1$s) Cancelar Aviso No se detecta Adobe Flash Player.\nPor favor, instálalo. Agente de usuario - Ruta de descargas + Ruta para las descargas Página de inicio personalizada Página web Borrar historial @@ -67,27 +68,27 @@ No Tamaño de la tipografía - Más grande + Muy grande Grande Normal Pequeño - Más pequeño + Muy pequeño Error No se ha detectado navegador del cual importar marcadores. Título - URL + Dirección URL Editar marcador Editar - Nueva pestaña privada + Nueva pestaña de incógnito Por defecto Atrás Buscar en la página Iniciando descarga\u2026 Sólo se puede descargar de URLs \"http\" o \"https\". - No hay tarjeta SD - Se requiere de almacenamiento USB para descargar el archivo. + No hay tarjeta SD + Se requiere de almacenamiento USB para descargar el archivo. Almacenamiento USB no disponible - El almacenamiento está ocupado. Para permitir las descargar, desactiva el almacenamiento USB desde el área de notificación. + El almacenamiento está ocupado. Para permitir las descargar, desactiva el almacenamiento USB desde el área de notificación. Habilitar cookies en el modo incógnito Adobe Flash Manual @@ -102,7 +103,7 @@ Se ha alcanzado el máximo de pestañas El texto se ha copiado al portapapeles El enlace se ha copiado al portapapeles - URL personalizada + Dirección personalizada Se ha bloqueado la carga del archivo local Licencias de código abierto (open source) Buscar @@ -113,10 +114,22 @@ Permitir No permitir Iniciar sesión - Usuario + Nombre de usuario Contraseña Sugerencias de búsqueda Proporcionadas por Google + Proporcionadas por DuckDuckGo + Sin sugerencias de búsqueda + Proxy HTTP + + Ninguno + Orbot + I2P + Manual + + Proxy manual + Host: + Puerto: Parece que tienes Orbot instalado. ¿Quieres usar Tor? Por favor, instala Orbot para usar Tor. @@ -127,7 +140,7 @@ Personalizado Sin título Mozilla Public License v. 2.0 - Freeware + Software gratuito Android Open Source Project hpHosts Ad Server List Pestaña reabierta @@ -135,17 +148,18 @@ Invertido Escala de grises Escala de grises invertida + Aumentar contraste Normal Sincronizar historial con Google - Selector de archivo + Selector de archivos NetCipher GNU Lesser General Public License - Exportar marcadores a la copia de seguridad - Importar marcadores a una copia de seguridad + Exportar marcadores a una copia de seguridad + Importar marcadores desde una copia de seguridad Marcadores exportados a Ajustes de marcadores No se pudo importar los marcadores del archivo - Elija un archivo + Elige un archivo Ajustes generales Ajustes de pantalla Ajustes de privacidad @@ -153,21 +167,81 @@ Detalles sobre la versión, autor y licencias. Cerrar pestaña Cerrar todas las pestañas + Cerrar las demás pestañas Bloquear cookies de terceras partes Habilitar modo de color Modo de lectura Cargando… - No se pudo extraer contenido de la página. + No se pudo cargar la página. Snacktory jsoup: Java HTML Parser MIT License Contenido de la caja de la URL + Codificación de texto - Dominio (por defecto) - URL - Título + Dominio (por defecto) + Dirección URL + Título Invertir color - Usar tema oscuro + Tema oscuro Pestañas + Tema de la app + Tema claro + Tema negro (AMOLED) + Nombre de la carpeta + Carpeta + Renombrar + Renombrar carpeta + ¿Qué te gustaría hacer con esta carpeta? + ¿Qué quieres hacer con este elemento del historial? + Borrar almacenamiento web + Borrar almacenamiento web al salir + Almacenamiento web borrado + Fuente de bloqueo de anuncios del archivo hosts + Ajustes de bloqueo de anuncios + Mostrar pestañas en el cajón de navegación + Cambiar marcador y tabuladores + Solicitar \'Do Not Track\' + Remover encabezados de identificación + Añadir a pantalla de inicio + Acceso directo añadido a la pantalla de inicio + Borrar todos los marcadores + + FAQ + Preguntas frecuentes + + + Ajustes de depuración + LeakCanary + Reinicia la aplicación para que el cambio tenga efecto. + + + Abrir en nueva pestaña + Abrir en pestaña de fondo + Abrir en pestaña incógnito + Remover marcador + Editar marcador + Remover del historial + Descargar imagen + Copiar enlace + Renombrar carpeta + Remover carpeta + Cerrar navegador + + I2P no está corriendo. + Los túneles I2P no están preparados aún. + Parece que tienes I2P instalado. ¿Quieres usar I2P? + + la fecha del certificado no es válida + el dominio del certificado no coincide con el dominio del sitio web + el certificado está caducado + el certificado no es válido + el certificado no es válido todavía + + el certificado no es confiable + La conexión a este sitio no es segura:\n%1$s\n¿Continuar en cualquier caso? + + La dirección no es válida, no se pudo descargar + No se pudo descargar en la localización específica diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0ac4949fe..2ca2c6775 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -16,7 +16,8 @@ --> - + + Lightning Nouvel onglet Partager @@ -28,7 +29,7 @@ Paramètres Accéder à ma position Enregistrer les mots de passe - User Agent + Agent utilisateur Activer Adobe Flash Page d\'accueil Mode plein écran @@ -50,6 +51,8 @@ Utiliser large fenêtre d\'affichage Charger les pages en mode aperçu Restaurer les onglets perdus au démarrage + Navigateurs pris en charge + Navigateur par défaut Aucun navigateur détecté Navigateur compatible détecté Masquer la barre d\'état lors de la navigation @@ -70,11 +73,11 @@ Personnalisé Moteur de recherche OK - Voulez-vous télécharger ce fichier ? + Voulez-vous télécharger ce fichier ? (%1$s) Annuler Avertissement Adobe Flash Player n\'a pas été détecté.\nVeuillez installer Flash Player. - User Agent + Agent utilisateur Téléchargement Page d\'accueil personnalisée Page internet @@ -144,6 +147,8 @@ Mot de passe Suggestions de recherche Proposé par Google + Propulsé par DuckDuckGo + Pas de suggestions de recherche Proxy HTTP Aucun @@ -175,6 +180,7 @@ Inversé Niveaux de gris Niveaux de gris inversés + Augmenter le contraste Normal Synchroniser l\'historique avec Google Explorateur de fichiers @@ -193,6 +199,7 @@ En savoir plus sur la version, l\'auteur et la licence. Fermer l\'onglet Fermer tous les onglets + Fermer les autres onglets Bloquer les cookies tiers Activer le mode couleur Mode lecteur @@ -208,9 +215,9 @@ Adresse Titre - Inverser la couleur - Thème sombre - Onglets + Inverser les couleurs + Thème sombre + Onglets Thème Thème clair Thème noir (AMOLED) @@ -225,6 +232,31 @@ Fichier Hosts source de blocage des publicités Blocage des publicités Afficher les onglets dans le volet de navigation + Échanger les favoris et les onglets Demander de \'Ne pas pister\' Supprimer les en-têtes d\'dentification + Ajouter à l\'écran d\'accueil + Ajouté à l\'écran d\'accueil + Supprimer tous les favoris + + FAQ + Questions fréquemment posées + + + Paramètres de déboggage + LeakCanary + Veuillez redémarrer l\'application pour prendre en compte les changements. + + + Ouvrir dans un nouvel onglet + Ouvrir en arrière plan + Ouvrir dans un onglet incognito + Supprimer le favori + Éditer le favori + Supprimer de l\'historique + Télécharger l\'image + Copier le lien + Renommer de dossier + Supprimer le dossier + Fermer le navigateur diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 636455492..49197e5fe 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -50,6 +50,8 @@ Széles nézőport használata Oldalak betöltése áttekinthető módban Elvesztett lapok visszaállítása induláskor + Támogatott Böngészők + Gyári Böngésző Nincs gyári böngésző észlelve Támogatott gyári böngésző észlelve Állapotsáv elrejtése böngészés közben @@ -60,7 +62,8 @@ Megnyitás Mit szeretnél csinálni ezzel a linkkel? Oldal megosztása - Mit szeretnél csinálni ezzel a könyvjelzővel? + Mit szeretnél csinálni, ezzel az előzménnyel? + Mit szeretnél csinálni, ezzel a könyvjelzővel? Törlés Üres oldal Alapértelmezett @@ -69,7 +72,7 @@ Egyéni Keresőmotor OK - Le szeretnéd tölteni ezt a fájlt? + Le szeretnéd tölteni ezt a fájlt? (%1$s) Mégse Figyelmeztetés Az Adobe Flash Player nincs észlelve.\nKérjük telepítse a Flash Player-t. @@ -101,6 +104,8 @@ Keresés az oldalon Letöltés elkezdése\u2026 Csak \"http\" vagy \"https\" URL-eket lehet letölteni. + Érvénytelen URL, nem sikerült a letöltés + Nem lehet letölteni a kiválasztott helyre Nincs SD kártya USB tárolóra van szükség a fájl letöltéséhez. USB-tár nem elérhető @@ -124,6 +129,13 @@ Nyílt forráskódú licencek Keresés Reklámok blokkolása + Kapcsolódás ehhez a weboldalhoz, nem biztonságos:\n%1$s\nFolytatja mindenképpen? + az tanúsítvány dátuma érvénytelen + a tanúsítvány lejárt + a domain a tanúsítványban, nem egyezik az domainjével + a tanúsítvány érvénytelen + a tanúsítvány, még nem érvényes + a tanúsítvány nem megbízható Űrlap újraküldése Szeretnél újból elküldi az adatokat? \nSzeretné használni a saját helyét? @@ -134,8 +146,21 @@ Jelszó Keresési javaslatok A Google támogatásával - Úgy tűnik, hogy önnek telepítve van az Orbot.Szeretné használni Tor-t? + HTTP Proxy + + Egyik sem + Orbot + I2P + Manuális + + Manuális proxy + Hoszt: + Port: + Úgy tűnik, hogy önnek telepítve van az Orbot. Szeretné használni a Tor-t? + Úgy tűnik, hogy önnek telepítve van az I2P. Szeretné használni az I2P-t? Kérjük telepítse az Orbot-ot annak érdekében, hogy működjön a proxy a Tor-ral. + I2P, nem fut. + I2P alagútak, nem állnak készen még. Igen Nem Cookie-ek törlése kilépéskor @@ -152,6 +177,7 @@ Negatív Szürkeárnyalatos Negatív szürkeárnyalatos + Kontraszt Növelése Normál Előzmények szinkronizálása Google-val Fájl választó @@ -170,6 +196,7 @@ Részletek az alkalmazás verziószámáról, a készítőről és a licencekről. Lap bezárása Összes lap bezárása + Másik lapok bezárása Harmadik féltől származó Cookie-k letiltása Színes mód engedélyezése Olvasó mód @@ -177,14 +204,34 @@ Nem sikerült semmit betölteni az oldalról. Snacktory jsoup: Java HTML Parser - MIT License + MIT Licenc URL doboz tartalma + Szöveg Kódolás - Domain (alapértelmezett) - URL - Cím + Domain (alapértelmezett) + URL + Cím - Negatív szín - Sötét téma - Lapok + Negatív szín + Sötét téma + Lapok + Alkalmazás Téma + Világos Téma + Fekete Téma (AMOLED) + Mappanév + Mappa + Átnevez + Mappa átnevezése + Mit szeretnél csinálni, ezzel a mappával? + Webtárhely törlése + Webtárhely törlése kilépéskor + Webtárhely törölve + Reklámblokkoló hosztfájl forrása + Reklámblokkoló beállítások + Oldalak mutatása a navigációs fiókban + \'Ne kövessen nyomon\' kérése + Azonosító fejléc eltávolítása + Hozzáadás a kezdőképernyőhöz + Parancsikon hozzáadva a kezdőképernyőhöz + Összes könyvjelző törlése diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 68adb044c..5683968a0 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,4 +1,5 @@ - + Impostazioni di debug + LeakCanary + Riavvia l\'applicazione per applicare le modifiche. + + + Apri in un\'altra scheda + Apri in una scheda in background + Apri in scheda in incognito + Rimuovi segnalibro + Modifica segnalibro + Rimuovi dalla cronologia + Scarica immagine + Copia indirizzo link + Rinomina cartella + Rimuovi cartella + Chiudi browser diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index ebb59f53f..59bca81af 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -20,64 +20,67 @@ 履歴 ブックマーク ブックマークに追加 - リンクをコピー + ページの URL をコピー 進む 設定 - 位置情報 + 位置情報の使用を許可する パスワードを記憶する ユーザーエージェント - Adobe Flash - スタートページ + Adobe Flash を有効にする + ホームページ 全画面モード - JavaScript - ダウンロード先のフォルダ - 高度な設定 + JavaScript を有効にする + ダウンロードフォルダ + 詳細 Apache License 2.0 - アプリケーションのバージョン + アプリのバージョン 終了時にキャッシュを消去する - 文字を右端で折り返す + テキストを右端で折り返す 画像を読み込まない - 新しいタブでページを開くことを許可する - Cookiesを有効にする - 他のブラウザからブックマークを読み込む + サイトが新しいタブを開くことを許可する + Cookie を有効にする + 他のブラウザからブックマークをインポートする 文字サイズ 推奨 検索エンジン 検索 - ワイドビューモードを使用する - オーバービューモードでページを読み込む - 開始時に前のタブを修復する - 読み込み可能なブラウザはありません - 読み込み可能なブラウザがあります - ブラウズ中ステータスバーを非表示にする - Cookiesを消去する - Clear Browser History + Wide view port を使用する + Overview mode でページを読み込む + 前回終了時のタブをアプリ起動時に復元する + サポートしているブラウザ + 標準ブラウザ + 標準ブラウザが見つかりません + サポートしている標準ブラウザが見つかりました + ステータスバーを非表示にする + Cookie を消去する + 履歴を消去する この画像をどうしますか? ダウンロード 開く このリンクをどうしますか? - このページを共有 + このページを共有します + この履歴項目をどうしますか? このブックマークをどうしますか? - 消去 - 空のページ - 標準 + 削除 + 空白ページ + デフォルト デスクトップ モバイル カスタム 検索エンジン OK - このファイルをダウンロードしますか? + このファイルをダウンロードしますか? (%1$s) キャンセル 警告 - Adobe Flash Playerをインストールしてください + Adobe Flash Player が見つかりません\nFlash Player をインストールしてください ユーザーエージェント - ダウンロード先 - カスタムスタートページ - Webpage + ダウンロードフォルダ + カスタムホームページ + ウェブページ 履歴を消去する - Cookiesを消去する + Cookie を消去する すべての履歴を消去しますか? - すべてのCookiesを消去しますか? + すべての Cookie を消去しますか? はい いいえ 文字サイズ @@ -87,96 +90,169 @@ 最小 エラー - どのブラウザからもブックマークが読み込めませんでした - 名前 + ブックマークをインポートするブラウザが見つかりません + タイトル URL ブックマークを編集する 編集 新しいシークレットタブ - 標準 + デフォルト 戻る - ページ内検索 - Starting download\u2026 - \"http\" や \"https\" から始まるURLのみダウンロードできます - SDカードがありません - ファイルをダウンロードするにはUSBストレージが必要です - USBストレージが利用不可です - ストレージがビジー状態です。ダウンロードするには、ステータスバーからUSB接続を変更してください。 - シークレットタブでCookiesを有効にする + ページ内を検索 + ダウンロードを開始します\u2026 + \"http\" または \"https\" で始まる URL のみダウンロードできます + 無効な URL です。ダウンロードできません + 指定したフォルダにダウンロードできません + SD カードがありません + ファイルをダウンロードするには USB ストレージが必要です + USB ストレージが使用できません + ストレージがビジー状態です。ダウンロードするには、ステータスバーの \"USB ストレージをオフにする\" をタップしてください + シークレットタブで Cookie を有効にする Adobe Flash - マニュアル + 手動 自動 - Contact Me - twitter.com/ACRDevelopment + 問い合わせ先 + twitter.com/RestainoAnthony キャッシュを消去する キャッシュを消去しました - ブックマークを読み込みました + ブックマークをインポートしました 履歴を消去しました - Cookiesを消去しました + Cookie を消去しました これ以上タブは開けません - クリップボードにテキストをコピーしました - クリップボードにリンクをコピーしました - カスタムURL - Local file has been blocked from loading - オープンソースライセンス - Search for - 広告ブロック - フォームの再送 - データを再送しますか? - \n位置情報の提供を許可しますか? + テキストをクリップボードにコピーしました + リンクをクリップボードにコピーしました + カスタム URL + ファイルを読み込めませんでした + オープンソース ライセンス + 検索 : + 広告をブロックする + このサイトへの接続は安全ではありません:\n%1$s\nそれでも続行しますか? + 証明書の日付が無効です + 証明書の期限が切れました + 証明書のドメインとサイトのドメインが一致しません + 証明書は無効です + 証明書はまだ有効ではありません + 証明書が信頼できません + フォームの再送信 + データを再送信しますか? + \n位置情報の使用を要求しています 許可する 許可しない サインイン ユーザー名 パスワード - 検索候補を表示する - Powered by Google - Orbotがインストールされています。Torを使用しますか? - Torでプロキシを有効にするにはOrbotをインストールしてください + 検索候補表示 + Google を使用する + DuckDuckGo を使用する + 検索候補を表示しない + HTTP プロキシ + + 使用しない + Orbot + I2P + 手動 + + 手動プロキシ + ホスト: + ポート: + Orbot はインストール済みです。Tor を使用しますか? + I2P はインストール済みです。I2P を使用しますか? + プロキシとして Tor を使用にするには Orbot をインストールしてください + I2P が動作していません + I2P トンネルの使用準備が整っていません はい いいえ - 終了時にCookiesを消去する + 終了時に Cookie を消去する 終了時に履歴を消去する - 規定 + デフォルト カスタム - Untitled + タイトルなし Mozilla Public License v. 2.0 Freeware Android Open Source Project hpHosts Ad Server List - 古いタブをもう1度開く + 閉じたタブを元に戻す レンダリングモード - 反転 + 色の反転 グレースケール - 反転・グレースケール + 反転グレースケール + 高コントラスト 通常 - Googleと履歴を同期する - File Chooser + Google と履歴を同期する + ファイルの選択 NetCipher GNU Lesser General Public License - バックアップにブックマークを書き出す - バックアップからブックマークを読み込む - ブックマークの書き出し先 - ブックマーク設定 - ファイルからブックマークを読み込めませんでした + ブックマークをエクスポートする + ブックマークをインポートする + ブックマークをエクスポートしました + ブックマーク + ブックマークのインポートに失敗しました ファイルを選択してください - 一般設定 - 表示設定 - 個人設定 - About - バージョン・開発者・ライセンスについての詳細 + 全般 + 表示 + プライバシー + このアプリについて + バージョン, 開発者, ライセンスなどの詳細 + 現在のタブを閉じる 全てのタブを閉じる - タブを閉じる + 他のタブを閉じる + サードパーティ Cookie を拒否する カラーモードを有効にする - フォルダ - リネーム - Webストレージをクリア - 終了時にWebストレージをクリア - フォルダ名 - 色を反転する リーダーモード + 読み込み中… + このページから何も読み出せませんでした + Snacktory + jsoup: Java HTML Parser + MIT License + URL 欄の表示内容 + テキストエンコーディング + + ドメイン名 (デフォルト) + URL + タイトル + + 色を反転する + ダーク タブ - サードパーティCookieを無効にする - ナビゲーションドロワでタブを表示する - URL-Boxコンテンツ + アプリのテーマ + ライト + ブラック (AMOLED) + フォルダ名 + フォルダ + 名前の変更 + フォルダ名の変更 + このフォルダをどうしますか? + Web storage を消去する + 終了時に Web storage を消去する + Web storage を消去しました + 広告ブロック用の Hosts ファイルの取得元 + 広告ブロック + タブをドロワーで表示する + ブックマークとタブのドロワーを入れ替える + \'Do Not Track\' を有効にする + Identifying Header を削除する + ホーム画面に追加 + ホーム画面にショートカットを追加しました + すべてのブックマークを削除する + + FAQ + よくある質問と回答 + + + デバッグ + LeakCanary + 変更を反映するにはアプリを再起動してください + + + 新しいタブで開く + バックグラウンドのタブで開く + シークレットタブで開く + ブックマークから削除する + ブックマークを編集する + 履歴から削除する + 画像をダウンロードする + リンクをコピーする + フォルダ名を変更する + フォルダを削除する + タブの消去 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 75d010c57..b1c7371c3 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -52,7 +52,7 @@ 임의의 UA 검색 엔진 확인 - 파일을 다운로드할까요? + 파일을 다운로드할까요? (%1$s) 취소 경고 Adobe Flash Player가 감지되지 않았습니다.\nFlash Player를 설치하세요. diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml new file mode 100644 index 000000000..7fbbbfb77 --- /dev/null +++ b/app/src/main/res/values-lt/strings.xml @@ -0,0 +1,43 @@ + + + Lightning + Pridėti žymę + Pridėti į pradžios langą + Leisti + Atgal + Tuščias puslapis + Žymės + Atšaukti + Kopijuoti nuorodą + Ištrinti + Neleisti + Atsiųsti + Aplankas + Susisiekti + Nauja kortelė + Nauja privati kortelė + Vadovas + Ne + Gerai + Atidaryti + Istorija + Numatytasis + Pirmyn + Keisti + Rasti puslapyje + Pervadinti + Dalintis + Tinklalapis + Taip + Naudotojo agentas + Default + Desktop + Mobile + Custom + FAQ + Taip + Skaitymo rėžimas + Ne + Normalus + Portas: + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 000000000..1220d29f5 --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,236 @@ + + + + + + Lightning + Nieuw Tabblad + Deel + Geschiedenis + Bladwijzers + Bladwijzer toevoegen + Kopieer link + Voorwaarts + Instellingen + Locatie toegang + Bewaar jouw paswoord + Gebruikersagent + Adobe Flash activeren + Startpagina + Volledig scherm modus + JavaScript activeren + Download naar locatie + Geavanceerde instellingen + Apache License 2.0 + Application versie + Wis cache bij beëindigen + Text herschikking activeren + Blokkeer afbeeldingen + Sta sites toe om in een nieuwe pagina te openen + Cookies activeren + Importeer bladwijzers van browser + Tekst grootte + Aanbevolen + Zoekmachine + Zoek + Gebruik wijde viewport + Laad pagina\'s in overzichtsweergave + Herstel tabbladen bij opstart + Ondersteunde browsers + Standaard browser + Geen standaard browser gevonden + standaard browser gevonden + Verberg status balk tijdens surfen + Wis browser cookies + Wis browser historiek + Wat wil je met deze afbeelding doen? + Download + Open + Wat wil je met deze link doen? + Deel deze pagina + Wat wil je met dit item doen? + Wat wil je met deze bladwijzer doen? + Verwijderen + Witte pagina + Standaard + Desktop + Mobile + Aangepast + Zoekmachine + OK + Wil je dit bestand downloaden? (%1$s) + Annuleer + Waarschuwing + Adobe Flash Player werd niet gevonden.\nGelieve Flash Player te installeren. + User Agent + Locatie downloaden + Aangepaste startpagina + Webpagina + Wis historiek + Wisk cookies + Wil je alle browser historiek wissen? + Wil je alle browser cookies wissen? + Ja + Nee + Tekst grootte + Grootst + Groot + Normaal + Smal + Smaller + Fout + Er werd geen browser gevonden om bladwijzers van te importeren. + Titel + URL + Wijzig bladwijzer + Wijzig + Nieuw incognito tabblad + Standaard + Terug + Vind in pagina + Download starten\u2026 + Kan alleen \"http\" of \"https\" URLs downloaden. + ongeldige URL gevonden, kan niet downloaden + Kan niet downloaden naar de bestemming + Geen SD kaart + USB opslag is vereist om bestand te downloaden. + USB opslag niet beschikbaar + Opslag is bezet. Om downloads toe te laten, gelieve USB opslag uit te zetten in het notificatiecentrum. + Cookies activeren in incognito modus + Adobe Flash + Manueel + Auto + Contacteer Mij + twitter.com/RestainoAnthony + Wis cache + Cache gewist + Bladwijzers zijn geïmporteerd + Historiek gewist + Cookies gewist + Maximum aantal tabbladen bereikt + Tekst gekopieerd naar klembord + Link gekopieerd naar klembord + Aangepaste URL + Lokaal bestand werd geblokkeerd + Open Source Licenses + Zoek voor + Blokkeer advertenties + Verbinding met deze site is niet veilig :\n%1$s\nToch doorgaan? + datum van certificaat is ongeldig + certificaat is vervallen + domein in certificaat komt niet overeen met site domein + certificaat is ongeldig + certificaat is nog niet geldig + certificaat wordt niet vertrouwd + Formulier herverzending + Wil je de gegevens opnieuw verzenden? + \nWil jouw locatie gebruiken + Toestaan + Niet toestaan + Inloggen + Gebruiksnaam + Paswoord + Ondersteund door Google + HTTP Proxy + + None + Orbot + I2P + Manual + + Manuele proxy + Host: + Poort: + Je lijkt Orbit te hebben geïnstalleerd. Wil je Tor gebruiken? + Je lijkt I2P te hebben geïnstalleerd. Wil je I2P gebruiken? + Gelieve Orbot te installeren om proxies te doen met Tor. + I2P draait niet. + I2P tunnels zijn niet klaar. + Ja + Nee + Wis cookies bij beëindigen + Wis geschiedenis bij beëindigen + Standaard + Aangepast + Geen titel + Mozilla Public License v. 2.0 + Freeware + Android Open Source Project + hpHosts Ad Server List + Oud tabblad heropend + Render modus + Omgekeerd + Zwart-wit + Omgkeerd zwart-wit + Contrast vergroten + Normaal + Synchroniseer geschiedenis met Google + Bestand kiezer + NetCipher + GNU Lesser General Public License + Exporteer bladwijzers naar backup + Importeer bladwijzers van backup + Bladwijzers geëxporteerd naar + Bladwijzer instellingen + Kon bladwijzers niet van bestand laden + Kies een bestand + Algemene instellingen + Scherm instellingen + Privacy instellingen + Over + Details over versie, auteur en licensie. + Sluit tabblad + Alle tabbladen sluiten + Alle andere tabbladen sluiten + Blokkeer derde partij cookies + Activeer kleurmodus + Leesmodus + Laden… + Kon niets van deze pagina laden. + Snacktory + jsoup: Java HTML Parser + MIT licensie + URL box inhoud + Tekst codering + + Domein (standaard) + URL + Titel + + Keer kleur om + Donker thema + Tabbladen + App thema + Licht thema + Zwart Thema (AMOLED) + Map naam + Map + Geef nieuwe naam + Geef map naam + Wat wil je met deze map doen? + Wis web opslag + Wis web opslag bij beëindigen + Web opslag gewist + Bron hosts bestand om advertenties te blokkeren + Advertentie blokkeer instellingen + Toon tabbladen in navigatie lade + Vraag \'Niet Volgen\' + Verwijder identificatie hoofden + Voeg aan startscherm toe + Snelkoppeling aan startscherm toegevoegd + Verwijder alle bladwijzers + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 3a3c32cbe..16fc7bcd1 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -53,7 +53,7 @@ Własny identyfikator Wyszukiwarka OK - Czy chcesz pobrać ten plik? + Czy chcesz pobrać ten plik? (%1$s) Anuluj Ostrzeżenie Adobe Flash Player nie został wykryty.\nProszę go zainstalować. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 000000000..e19b5d957 --- /dev/null +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,201 @@ + + + + + Lightning + Nova aba + Compartilhar + Histórico + Favoritos + Adicionar favoritos + Copiar link da página + Avançar + Configurações + Acesso de localização + Salvar senhas + Agente + Ativar Adobe Flash + Página inicial + Modo de tela completa + Ativar JavaScript + Pasta de download + Configurações avançadas + Apache License 2.0 + Versão da aplicação + Limpar cache ao sair + Ativar ajuste de texto + Bloquear imagens + Permitir que sites abram novas janelas + Ativar cookies + Importar favoritos do navegador + Tamanho do texto + Recomendado + Mecanismo de pesquisa + Pesquisa + Utilizar viewport amplo + Carregar páginas no modo panorâmico + Restaurar abas ao iniciar + Navegador não detectado + Navegador detetado + Ocultar barra de estado ao navegar + Limpar cookies + Limpar histórico + O que gostaria de fazer com esta imagem? + Salvar + Abrir + O que gostaria de fazer com este link? + Compartilhar esta página + O que gostaria de fazer com este favorito? + Remover + Página vazia + Padrão + Desktop + Mobile + Personalizado + Mecanismo de pesquisa + OK + Gostaria de transferir este arquivo? (%1$s) + Cancelar + Aviso + O Adobe Flash Player não foi detectado.\nNecessário instalar o Flash Player. + Agente de utilizador + Local das transferências + Página inicial personalizada + Página web + Limpar histórico + Limpar cookies + Gostaria de limpar todo o histórico do navegador? + Gostaria de limpar todos os cookies do navegador? + Sim + Não + Tamanho do texto + Maior + Grande + Normal + Pequeno + Menor + Erro + Não foi detectado qualquer navegador para importar os favoritos. + Título + URL + Editar favorito + Editar + Nova aba anônima + Padrão + Voltar + Localizar na página + A iniciar transferência\u2026 + Apenas possível para as URLs \"http\" ou \"https\". + Cartão SD não encontrado + Necessita-se de um cartão SD para salvar o arquivo transferido. + Cartão SD não disponível + O cartão SD está ocupado. Para poder transferir arquivos, desative a opção Desativar armazenamento USB na notificação. + Permitir cookies no modo anônimo + Adobe Flash + Manual + Automático + Contato + twitter.com/RestainoAnthony + Limpar cache + Cache limpo + Favoritos importados + Histórico removido + Cookies removidos + Atingido número máximo de abas + Texto copiado para a área de transferência + Link copiado para a área de transferência + URL personalizado + Bloqueado o carregamento do arquivo local + Licenças Open Source + Pesquisar por + Bloquear anúncios + Submissão de formulário + Gostaria de reenviar os dados? + \nGostaria de utilizar a sua localização + Sim + Não + Iniciar sessão + Usuário + Senha + Disponibilizado por Google + Proxy HTTP + + Não + Orbot + I2P + Manual + + Proxy manual + Servidor: + Porta: + Parece que você tem o Orbot instalado. Gostaria de utilizar a rede Tor? + Parece que você tem o I2P instalado. Gostaria de utilizar a rede I2P? + Por favor instale o Orbot para poder utilizar a rede Tor. + I2P não está em execução. + Os canais I2P ainda não estão prontos. + Sim + Não + Limpar cookies ao sair + Limpar histórico ao sair + Padrão + Personalizado + Sem título + Mozilla Public License v. 2.0 + Freeware + Projeto Android Open Source + hpHosts Ad Server List + Reabrir última aba + Modo de renderização + Invertido + Escala cinza + Escala cinza invertida + Normal + Sincronizar histórico com Google + Seletor de arquivos + NetCipher + GNU Lesser General Public License + Exportar favoritos para backup + Importar favoritos de um backup + Favoritos salvos em + Configurações de favoritos + Não foi possível importar os favoritos do arquivo + Escolha um arquivo + Configurações gerais + Configurações de exibição + Configurações de privacidade + Sobre + Detalhes da versão, do autor e da licença + Fechar aba + Fechar todas as abas + Bloquear cookies de terceiros + Ativar modo de cor + Modo de leitura + A carregar… + A página não foi carregada. + Snacktory + jsoup: Java HTML Parser + MIT License + Conteúdo da caixa do URL + + Domínio (padrão) + URL + Título + + Inverter cores + Tema escuro + Abas + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 5acdcff42..ba4356986 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -32,35 +32,38 @@ Página inicial Modo de ecrã completo Ativar JavaScript - Local de transferências + Local para descargas Definições avançadas Apache License 2.0 Versão da aplicação Limpar cache ao sair Ativar ajuste de texto Bloquear imagens - Permitir que os sítios web abram novas janelas + Permitir que os sites abram novas janelas Ativar cookies Importar marcadores do navegador Tamanho do texto Recomendado - Mecanismo de pesquisa + Motor de pesquisa Pesquisa Utilizar viewport amplo Carregar páginas no modo panorâmico Restaurar separadores ao iniciar + Navegadores suportados + Navegador do sistema Navegador não detetado Navegador detetado Ocultar barra de estado ao navegar Limpar cookies Limpar histórico O que gostaria de fazer com esta imagem? - Transferir + Descarregar Abrir O que gostaria de fazer com esta ligação? Partilhar esta página + O que gostaria de fazer com este item do histórico? O que gostaria de fazer com este marcador? - Eliminar + Apagar Página vazia Padrão Desktop @@ -68,12 +71,12 @@ Personalizado Mecanismo de pesquisa OK - Gostaria de transferir este ficheiro? + Deseja descarregar este ficheiro? (%1$s) Cancelar Aviso O Adobe Flash Player não foi detetado.\nTem que instalar o Flash Player. Agente de utilizador - Local das transferências + Local para as descargas Página inicial personalizada Página web Limpar histórico @@ -98,12 +101,14 @@ Padrão Recuar Localizar na página - A iniciar transferência\u2026 + A iniciar descarga\u2026 Apenas possível para os URL \"http\" ou \"https\". + URL inválido e a descarga não é possível + Não é possível descarregar da localização especificada Cartão SD não encontrado - Necessita de um cartão SD para guardar o ficheiro transferido. + Necessita de um cartão SD para guardar o ficheiro descarregado. Cartão SD não disponível - O cartão SD está ocupado. Para poder transferir ficheiros, desative a opção Desativar armazenamento USB na notificação. + O cartão SD está ocupado. Para poder descarregar ficheiros, desative a opção Desativar armazenamento USB na notificação. Permitir cookies no modo incógnito Adobe Flash Manual @@ -115,7 +120,7 @@ Marcadores importados Histórico removido Cookies removidos - Atingido número máximo de separadores + Atingido o número máximo de separadores Texto copiado para a área de transferência Ligação copiada para a área de transferência URL personalizado @@ -123,6 +128,13 @@ Licenças Open Source Pesquisar por Bloquear anúncios + A ligação a este site não é segura:\n%1$s\nContinuar? + data de certificado inválida + certificado expirado + domínio do certificado não coincide com o domínio do site + certificado inválido + certificado ainda não válido + certificado não confiável Submissão de formulário Gostaria de reenviar os dados? \nGostaria de utilizar a sua localização @@ -132,7 +144,9 @@ Utilizador Palavra-passe Sugestões de pesquisa - Disponibilizado por Google + Disponibilizadas por Google + Disponibilizadas por DuckDuckGo + Sem sugestões de pesquisa Proxy HTTP Não @@ -153,7 +167,7 @@ Limpar cookies ao sair Limpar histórico ao sair Padrão - Personalizado + Personalizada Sem título Mozilla Public License v. 2.0 Freeware @@ -164,6 +178,7 @@ Invertido Escala de cinzento Escala de cinzento invertida + Aumentar contraste Normal Sincronizar histórico com Google Seletor de ficheiros @@ -178,10 +193,11 @@ Definições gerais Definições de exibição Definições de privacidade - Sobre + Acerca Detalhes da versão, do autor e da licença Fechar separador Fechar todos os separadores + Fechar os outros separadores Bloquear cookies de terceiros Ativar modo de cor Modo de leitura @@ -189,8 +205,9 @@ A página não foi carregada. Snacktory jsoup: Java HTML Parser - MIT License + Licença MIT Conteúdo da caixa do URL + Codificação do texto Domínio (padrão) URL @@ -199,4 +216,45 @@ Inverter cores Tema escuro Separadores + Tema da aplicação + Tema claro + Tema preto (AMOLED) + Nome da pasta + Pasta + Renomear + Renomear pasta + O que gostaria de fazer com esta pasta? + Limpar armazenamento web + Limpar armazenamento web ao sair + Armazenamento web removido + Fonte do ficheiro hosts para Ad Blocking + Definições AdBlock + Mostrar separadores no menu de navegação + Trocar menus de marcadores e separadores + Pedidos \'Do Not Track\' + Remover cabeçalhos de identificação + Adicionar ao ecrã + Atalho adicionado ao ecrã + Apagar todos os marcadores + + FAQ + Perguntas frequentes + + + Definições de depuração + LeakCanary + Reinicie a aplicação para aplicar as alterações. + + + Abrir em novo separador + Abrir em separador de fundo + Abrir em separador incógnito + Remover marcador + Editar marcador + Remover do histórico + Descarregar imagem + Copiar ligação + Renomear pasta + Remover pasta + Fechar navegador diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 93957fe60..c55010b57 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -12,37 +12,40 @@ Местоположение Сохранять пароли User Agent - Adobe Flash + Включить Adobe Flash Новая вкладка Во весь экран - JavaScript - Папка загрузок + Включить JavaScript + Путь загрузки Дополнительно Apache License 2.0 Версия Очищать кэш при выходе Перекомпоновка Без изображений - Всплывающие окна - Cookies + Разрешить всплывающие окна + Включить куки Импорт закладок Размер текста - рекомендуется + Рекомендуется Поисковая система Поиск Широкий вид Загружать обзор Восстановление предыдущей сессии + Поддерживаемые браузеры + Встроенный браузер Встроенный браузер не обнаружен Встроенный браузер обнаружен Без строки состояния - Очистить cookies + Очистить куки Очистить историю Выберите действие для изображения Загрузить Открыть Выберите действие для адреса Способ передачи + Выберите действие для элемента истории Выберите действие для закладки Удалить Пустая страница @@ -52,18 +55,18 @@ Выбрать Поисковая система OK - Загрузить файл? + Загрузить файл? (%1$s) Отмена Внимание "Adobe Flash Player не найден.\nУстановите Flash Player" User Agent - Папка загрузок + Путь загрузки Введите адрес Открыть адрес - Очистка истории - Очистка Cookies + Очистить историю + Очистить куки История будет удалена. Продолжить? - Cookies будут удалены. Продолжить? + Куки будут удалены. Продолжить? Да Нет Размер текста @@ -82,23 +85,25 @@ Домой Назад Найти на странице - Загрузка… + Загрузка\u2026 Загрузка возможна только с \"http\" или \"https\" адресов + Некорректный URL, загрузка невозможна + Невозможно загрузить по указанному пути Нет карты памяти Для загрузки файла необходима карта памяти Карта памяти недоступна - Карта памяти подключена к ПК. Для начала загрузки отключите её от ПК, нажав на уведомление - Включить Cookies в режиме Инкогнито + Карта памяти подключена к ПК. Для начала загрузки отключите её от ПК, нажав на уведомление. + Включить куки в режиме инкогнито Adobe Flash Вручную Автоматически Связаться с разработчиками - twitter.com/ACRDevelopment + twitter.com/RestainoAnthony Очистить кэш Кэш очищен Закладки импортированы История очищена - Cookies очищены + Куки очищены Достигнуто максимальное число вкладок Текст скопирован в буфер обмена Ссылка скопирована в буфер обмена @@ -107,9 +112,16 @@ Open Source Licenses Искать Блокировать рекламу - Повторная форма - Вы действительно хотите отправить данные? - \Хотите использовать своё местоположение? + Соединение не защищено:\n%1$s\nВсё равно продолжить? + дата сертификата недействительна + истёк срок действия сертификата + имя сертификата не соответствует имени сайта + сертификат недействителен + сертикат ещё не действителен + сертификат не является доверенным + Отправка формы + Вы действительно хотите повторно отправить данные? + \nХотите использовать своё местоположение? Разрешить Не разрешать Вход @@ -117,12 +129,25 @@ Пароль Подсказки поиска Используя Google + HTTP прокси + + Нет + Orbot + I2P + Вручную + + Прокси + Хост: + Порт: Похоже, установлен Orbot. Вы хотите использовать Tor? + Похоже, установлен I2P. Вы хотите использовать I2P? Пожалуйста, установите Orbot для проксирования через Tor. + I2P не запущен. + I2P туннели ещё не готовы. Да Нет - Очистить cookies при выходе - Очистить историю при выходе + Очищать куки при выходе + Очищать историю при выходе По умолчанию Выбрать Без названия @@ -135,6 +160,7 @@ Инвертированный Оттенки серого Инвертированные оттенки серого + Повышенный контраст Нормальный Синхронизировать историю с Google Выбор файла @@ -151,4 +177,50 @@ Настройки приватности О программе Информация о версии, авторах и лицензиях - + Закрыть вкладку + Закрыть все вкладки + Закрыть остальные вкладки + Блокировать куки со сторонних сайтов + Включить цветной режим + Режим чтения + Загрузка… + Не удалось загрузить страницу. + Snacktory + jsoup: Java HTML Parser + MIT License + Содержимое поля URL + Кодировка текста + + Доменное имя (по умолчанию) + URL + Заголовок + + Инвертировать цвета + Тёмная тема + Вкладки + Тема приложения + Светлая тема + Чёрная тема (AMOLED) + Имя директории + Директория + Переименовать + Переименовать директорию + Выберите действите для директории + Очистить интернет-хранилище + Очищать интернет-хранилище при выходе + Интернет-хранилище очищено + Показывать владки в боковом меню + Запрашивать \'Do Not Track\' + Удалять идентифиц. заголовки + Добавить на дом. экран + Ярлык добавлен на домашний экран + Удалить все закладки + + FAQ + Часто задаваемые вопросы + + + Настройки отладки + LeakCanary + Чтобы изменения вступили в силу, необходимо перезапустить приложение. + \ No newline at end of file diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 997be3955..f4324a1bf 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -42,7 +42,7 @@ Блокирај слике Дозволи сајтовима да отварају нове прозоре Омогући колачиће - Увези обележиваче из Прегледача + Увези обележиваче из прегледача Величина текста Препоручљиво Мотор претраге @@ -50,6 +50,8 @@ Широки приказ Учитај странице у режиму прегледа Врати изгубљене језичке по покретању + Подржани прегледачи + Уграђени прегледач Нема уграђеног прегледача Откривен подржани уграђени прегледач Сакриј траку стања током прегледања @@ -70,7 +72,7 @@ Посебна Мотор претраге У реду - Желите ли да преузмете овај фајл? + Желите ли да преузмете овај фајл? (%1$s) Одустани Упозорење Адобов Флеш није откривен.\nИнсталирајте Флеш Плејер. @@ -170,11 +172,12 @@ Фривер Андроид пројекат отвореног кôда hpHosts списак рекламних сервера - Поново отворени претходни језичци + Поново отворени претходни језичак Исцртавање Инверзно Сивило Инверзно сивило + Појачан контраст Нормално Синхронизуј историјат са Гуглом Бирач фајлова @@ -193,6 +196,7 @@ Детаљи о издању, аутору и лиценци. Затвори језичак Затвори све језичке + Затвори друге језичке Блокирај колачиће треће стране Режим боје Режим за читање @@ -225,4 +229,9 @@ Извор hosts фајла за блокирање реклама Поставке Адблока Језичци у фиоци навигације + Захтевај „Не прати ме“ + Уклони идентификујућа заглавља + Додај на почетни екран + Пречица додата на почетни екран + Обриши све обележиваче diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index d18fdde7d..a70a21b10 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,6 +1,5 @@ - Lightning Yeni Sekme Paylaş @@ -18,10 +17,10 @@ Tam ekran modu Javascript\'i aç İndirme Yeri - - Apache License 2.0 + Gelişmiş Ayarlar + Apache Lisans 2.0 Uygulama Sürümü - Çıkarken geçici belleği temizle + Çıkışta önbelleği temizle Yazıyı sayfaya göre ayarla Resimleri Kapat Sitelerin yeni sekme açmasına izin ver @@ -34,17 +33,20 @@ Geniş ekranda kullan Sayfaları önizleme modunda göster Açılırken önceki sekmeleri geri yükle + Desteklenen Tarayıcılar + Stock Tarayıcı Stock tarayıcı bulunamadı Desteklenen stock tarayıcı bulundu Gezinirken durum çubuğunu gizle Tarayıcı çerezlerini temizle - Clear Browser History + Tarayıcı Geçmişini Temizle Bu resimle ne yapmak istersin? İndir Bu bağlantıyla ne yapmak istersin? Bu sayfayı paylaş - Bu yer işaretiyle ne yapmak istersin? + Geçmiş öğelerini ne yapmak istersin? + Bu yer işaretini ne yapmak istersin? Sil Boş Sayfa Varsayılan @@ -53,7 +55,8 @@ Kişisel Arama Motoru Tamam - Bu dosyayı indirmek ister misin? + Bu dosyayı indirmek ister misin? (%1$s) + Bilinmeyen Boyut İptal Uyarı Adobe Flash Player bulunamadı.\nLütfen Flash Player yükleyin. @@ -85,16 +88,18 @@ Sayfada Bul İndirme başlatılıyor\u2026 Yalnızca \"http\" veya \"https\" bağlantıları indirilebilir. - Hafıza kartı bulunamadı - Bu dosyayı indirmek için USB depolama gereklidir. + Geçersiz URL, yükleme yapılamıyor + Belirtilen yere indirme yapılamıyor + Hafıza kartı bulunamadı + Bu dosyayı indirmek için USB depolama gereklidir. USB depolama erişilemez - Depolama meşgul. İndirmeleri aktif etmek için bildirimler ekranından USB depolamayı kapatın. + Depolama meşgul. İndirmeleri aktif etmek için bildirimler ekranından USB depolamayı kapatın. Gizli modda çerezleri aktif et Adobe Flash El ile Otomatik Benimle iletişim kur - twitter.com/ACRDevelopment + twitter.com/RestainoAnthony Önbelleği temizle Önbellek temizlendi Yer işaretleri içe aktarıldı @@ -105,21 +110,44 @@ Bağlantı panoya kopyalandı Kişisel bağlantı Yerel dosyanın yüklenmesi engellendi - Open Source Licenses + Açık Kaynak Lisansları Ara Reklamları Engelle + Bağlantı güvenli değil:\n%1$s\nDevam etmek istiyor musun? + sertifika tarihi geçersiz + sertifika süresi doldu + sertifika alan adı site etki alanıyla eşleşmiyor + sertifika geçersiz + sertifika henüz geçerli değil + sertifika güvenilir değil Formu yeniden gönder Veriyi yeniden göndermek istiyor musun? \nKonum bilgisi isteniyor İzin ver İzin verme Giriş Yap + Sunucu mesajı: %s Kullanıcı adı Parola Önerilenleri ara - Powered by Google + Google tarafından desteklenmektedir + DuckDuckGo tarafından desteklenmektedir + Arama önerisi yok + HTTP Proxy + + Yok + Orbot + I2P + El ile + + El ile proxy + Host: + Port: Cihazında Orbot yüklü görünüyor. Tor kullanmak ister misin? + Cihazında I2P yüklü görünüyor. I2P kullanmak ister misin? Tor proxy ağı ile gezinmek için Orbot yüklemelisin. + I2P çalışmıyor. + I2P tünelleri hazır değil. Evet Hayır Çıkarken çerezleri temizle @@ -127,15 +155,16 @@ Varsayılan Kişisel Başlıksız - Mozilla Public License v. 2.0 + Mozilla Kamu Lisansı v. 2.0 Ücretsiz - Android Open Source Project - hpHosts Reklam Server Listesi + Android Açık Kaynak Projesi + hpHosts Reklam Sunucu Listesi Önceki sekmeler yeniden açıldı Render Modu Ters-Düz Gri tonlamalı Ters-Düz ve Geri tonlamalı + Kontrastı arttırın Normal Geçmişi Google ile eşitle Dosya seçimi @@ -147,4 +176,66 @@ Yer işaretleri ayarları Yer işaretleri dosyadan yüklenemedi Dosya seç + Genel + Görüntü + Gizlilik + Hakkında + Sürüm, yazar ve lisansla ilgili ayrıntılar. + Geçerli sekmeyi kapat + Tüm sekmeleri kapat + Diğer sekmeleri kapat + 3. Taraf Çerezlerini Engelleyin + Renk modunu etkinleştir + Okuyucu Modu + Yükleniyor… + Sayfadan bir şey yüklenemedi. + Snacktory + jsoup: Java HTML Parser + MIT License + URL Kutusu İçeriği + Metin Kodlama + + Alan adı (varsayılan) + URL + Başlık + + Rengi ters çevir + Karanlık Tema + Siyah durum çubuğu + Sekmeler + Uygulama Teması + Hafif Tema + Siyah Tema (AMOLED) + Klasör Adı + Klasör + Yeniden Adlandır + Klasörü Yeniden Adlandır + Bu klasörle ne yapmak istersiniz? + Depolama alanını temizle + Çıkarken depolama alanını temizle + Depolama alanı temizlendi + Dosya reklamını engelleyen kaynağı barındırır + Reklam Engelleme Ayarları + Gezinme sekmelerini göster + Yer işaretini ve sekmeyi değiştir + İstek \'Takip etmeyin\' + Başlık belirtecini kaldır + Ana ekrana kısayol eklendi + Tüm yer işaretlerini sil + S.S.S + Sık Sorulan Sorular + Hata Ayıklama Ayarları + LeakCanary + Değişikliklerin etkili olması için lütfen uygulamayı yeniden başlatın. + Yeni sekmede aç + Arka planda aç + Gizli sekmede aç + Yer İşaretini Kaldır + Yer İşaretini Düzenle + Geçmişi kaldır + Resmi İndir + Linki kopyala + Klasörü yeniden adlandır + Klasörü kaldırın + Tarayıcıyı Kapat diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml index bb3d86776..ad8837743 100644 --- a/app/src/main/res/values-v21/styles.xml +++ b/app/src/main/res/values-v21/styles.xml @@ -11,7 +11,7 @@ @null @color/primary_color_dark - + @color/secondary_color_settings_dark @@ -24,7 +24,7 @@