311 Commits

Autor SHA1 Mensagem Data
Anthony Restaino dffd572afc Fix memory leaks caused by the android framework 2016-06-01 20:05:55 -04:00
Anthony Restaino b7f3defd19 Fix bug where AutoCompleteTextView selected text but didn't delete it when typing
Also added in window focus change callback so that we can animate UI in
correctly. Also other small changes
2016-05-25 21:35:38 -04:00
Anthony Restaino e11a718d3b Updated to gradle wrapper 2.10, updated to android gradle plugin 2.1 2016-05-19 22:54:15 -04:00
Anthony Restaino a47cede6c5 Updating gradle dependencies 2016-05-19 22:52:52 -04:00
Anthony Restaino b1a8b7a0d5 Add debug settings so that you can toggle LeakCanary (and other settings in the future) in debug 2016-05-09 21:52:18 -04:00
Anthony Restaino aca3b6c08a Fix CI build 2016-04-23 09:54:09 -04:00
Anthony Restaino 69dba8d5f1 release bump and update tools version 2016-04-22 12:19:58 -04:00
Anthony Restaino eda498c65f Fixed bug where onComplete would throw an error if onError was called before 2016-04-22 11:56:12 -04:00
Anthony Restaino d80e7e2edc Fixed bug with WebView onResume, improved Observable, fixed some other stuff 2016-04-21 20:28:44 -04:00
Anthony Restaino f6c818fbb5 Fixed bug with state restoration, fixed weird tab closing behavior, added some missing annotations 2016-04-21 09:04:49 -04:00
Anthony Restaino d59aeef3a9 added some missing nullable annotation additions, switched to compat implementations of some methods, fixed some lint warnings 2016-04-18 14:00:09 -04:00
Anthony Restaino dbd7e6c2e6 Updated dependencies 2016-04-18 13:58:54 -04:00
Anthony Restaino d75675e006 More suggestions cleanup 2016-04-17 00:17:46 -04:00
Anthony Restaino 7a256707a7 Cleaned up search suggestions code, fixed potential memory leaks 2016-04-17 00:11:34 -04:00
Anthony Restaino 674ebb88f9 document BrowserPresenter, fix some things with it 2016-04-16 20:58:17 -04:00
Anthony Restaino d6a1450bef Update to latest support library 2016-04-16 20:26:09 -04:00
Anthony Restaino 99c09a9d63 fixed bug with event bus, moved adapter 2016-04-16 20:05:15 -04:00
Anthony Restaino f322c570c0 Fix bug where keyboard would hide text boxes in incognito 2016-04-07 10:18:14 -04:00
Anthony Restaino 984aa133ec Fixed null pointer exception, fixed threading bug on ICS, upgraded leak canary version 2016-04-04 21:59:35 -04:00
Anthony Restaino 008e61b5a8 Bugfix bump... hopefully last one before merge to master and full release 2016-03-28 20:12:25 -04:00
Anthony Restaino 6d47d7232f Fixed null pointer exception 2016-03-28 19:59:42 -04:00
Anthony Restaino e9d01dc104 Revert to previous support library version until they fix the menu bug 2016-03-28 19:54:45 -04:00
Anthony Restaino a7748ceee2 Added FAQ to settings 2016-03-24 23:02:25 -04:00
Anthony Restaino 6e940b0a15 Only Kitkat and up supports changing headers, disable on lower API versions 2016-03-24 21:41:31 -04:00
Anthony Restaino cabea7e097 Fixed bug where bookmarks as homepage changes what bookmarks are shown in bookmark drawer 2016-03-24 21:06:04 -04:00
Anthony Restaino 4d400f995f bugfix bump 2016-03-23 22:05:44 -04:00
Anthony Restaino bd98619d4f Fixed bug where keyboard would cover text input on the webview 2016-03-23 21:53:05 -04:00
Anthony Restaino 40cda1317a Don't enable useless autocomplete box to popup on the embedded search form 2016-03-23 21:51:32 -04:00
Anthony Restaino e26330a5bd Fixed threading bug in bookmark setting fragment, changed default bookmark title 2016-03-23 20:20:40 -04:00
Anthony Restaino dbc186db9b Fixed bugs related restore tabs option
- fixed tab being blank if option was off
- fixed bug where bookmarks would show sub folder if browser was closed
in that folder if option was on
2016-03-23 20:10:42 -04:00
Anthony Restaino d7faeaa2fc Update version code in prep for release, enable color mode by default 2016-03-22 23:39:06 -04:00
Anthony Restaino ae6726b290 Remove unnecessary imports, make methods static 2016-03-22 21:16:11 -04:00
Anthony Restaino f05312e915 Fixed bug where fragments wouldn't update their preferences if they changed 2016-03-22 21:12:17 -04:00
Anthony Restaino 58d8cb6a36 Fixed null pointer exception by properly replacing the fragment 2016-03-21 22:10:16 -04:00
Anthony Restaino 57d5298bec Fix bug where certain devices had different toolbar heights 2016-03-20 16:44:45 -04:00
Anthony Restaino bd8c439161 Fixed bug where multiple processes caused incognito to nor respond to preference changes.
Possibly in the future I should explore gong back to multiprocess for
incognito mode but right now it causes bugs. Also tweaked UI color in
color mode
2016-03-20 13:07:50 -04:00
Anthony Restaino f90ab177d5 Color the search bar appropriately for the various theme/color mode, fixed bug when restarting activity 2016-03-19 13:16:53 -04:00
Anthony Restaino 1685a13df3 Fixed some bugs with restoring/initializing tabs when new intents were received and browser was killed by background 2016-03-18 00:00:30 -04:00
Anthony Restaino 87ae1eb8fe Fixed bug where changing theme resulted in default tab icons being incorrectly themed 2016-03-16 21:20:26 -04:00
Anthony Restaino 8f230e3550 Fixed bug when switching between hiding status bar on/off 2016-03-16 20:57:12 -04:00
Anthony Restaino 3e8f3b2702 Fixed layout bug in full screen mode 2016-03-14 23:31:53 -04:00
Anthony Restaino 8bcb3668c0 updated hosts file 2016-03-13 18:16:23 -04:00
Anthony Restaino b8b2bd090f Fixed crashes 2016-03-13 18:13:19 -04:00
Anthony Restaino 14f08a8fef Update to latest support library, improve drawer closing experience 2016-03-10 22:23:32 -05:00
Anthony Restaino d3ac7187bb Roll back support library until fragment backgrounds are fixed 2016-03-09 22:54:48 -05:00
Anthony Restaino 2f8feead71 Fixed another IO not closed resource leak 2016-03-09 22:53:48 -05:00
Anthony Restaino ee6314f521 Fixed bug with fragment background 2016-03-09 22:53:11 -05:00
Anthony Restaino caa0de84ce Fixed leaked io connection 2016-03-09 22:46:00 -05:00
Anthony Restaino 97a64401e8 Fixed layout bug, fixed bug slowing down recyclerview animations 2016-03-06 14:34:01 -05:00
Anthony Restaino 3833fdb449 Fixing some animation bugs 2016-03-04 23:00:51 -05:00
Anthony Restaino 9c3607aa3d Temporarily revert v4 support until its fixed, add some documentation 2016-03-03 23:16:30 -05:00
Anthony Restaino 3fe2761552 Temporarily revert appcompat dependency until bugs are fixed 2016-03-02 22:00:36 -05:00
Anthony Restaino 8763f35668 Update gradle version, fix crash on kitkat caused by old gradle plugin version 2016-03-02 21:39:53 -05:00
Anthony Restaino 1198aeeb4d fixed index out of bounds bug 2016-03-01 22:10:18 -05:00
Anthony Restaino 6308677438 Smoothly close browser by closing the activity after the drawers are closed, removed bus event 2016-03-01 22:07:55 -05:00
Anthony Restaino e0ace14029 Fixed bug where rotating device caused webview height to be incorrect 2016-03-01 21:46:39 -05:00
Anthony Restaino 9ea98e13ad Removed layer of overdraw 2016-02-24 19:39:30 -05:00
Anthony Restaino a6edd3ca29 Update to latest support libs, get rid of pointless hardware layers 2016-02-24 19:33:09 -05:00
Anthony Restaino 8132b34bbf Fixed potential memory leak, moved variables 2016-02-24 19:18:05 -05:00
Anthony Restaino 486078a7d1 Fixed bug where new intents wouldn't open in the browser if it had been killed by the OS 2016-02-24 19:15:01 -05:00
Anthony Restaino 7486ebe3c4 Fixed memory leak, removed useless log statement 2016-02-21 19:22:19 -05:00
Anthony Restaino b2794b9d11 Animate vertical and horizontal tabs correctly 2016-02-21 16:52:25 -05:00
Anthony Restaino f98f45225c Fixed bug where searching text in page the arrows did the opposite of what you thought 2016-02-21 15:11:45 -05:00
Anthony Restaino 2c4db0c54b Animate tab addition/deletion in recyclerview, change full screen implementation to be simpler
TODO still need to use correct animations for tab addition and removal
2016-02-21 15:11:20 -05:00
Anthony Restaino d3ead42f8e fix rotation bug 2016-02-12 21:51:56 -05:00
Anthony Restaino 71a6c93551 Fixed toolbar size bug on rotation 2016-02-12 21:38:16 -05:00
Anthony Restaino 737c02d6e8 Added back/forward icon enable/disabling on tablet devices 2016-02-12 17:36:46 -05:00
Anthony Restaino f2d2c8ed5f Moved icon in drawer, removed unnecessary view in tab_list_item 2016-02-12 17:32:34 -05:00
Anthony Restaino 0ab302775c Fixed NPE in bookmarks fragment... ugh 2016-02-12 17:31:56 -05:00
Anthony Restaino fd5c26cc52 Add support for guardian project panic/ripple app 2016-02-12 09:05:39 -05:00
Anthony Restaino 19b6a5bfc5 Fixed bug where tab icon showed in desktop tab mode 2016-02-11 21:41:46 -05:00
Anthony Restaino 45df40f580 Merge pull request #375 from MarkThat/patch-1
Update italian translation
2016-02-11 21:06:48 -05:00
Anthony Restaino a1978c73b8 Merge pull request #369 from rishubil/dev
Add increase contrast filter
2016-02-11 21:06:00 -05:00
Anthony Restaino 6e76e7d430 fix leaked tab listener 2016-02-10 18:47:13 -05:00
Anthony Restaino 9b34a553ed Just log an error instead of crashing when view state is messed up 2016-02-09 23:06:13 -05:00
Anthony Restaino c9323cc7fd Temporarily fix bug where tabs created outside the presenter class (in manager) cause the tab number to be mismatched 2016-02-09 21:10:51 -05:00
Anthony Restaino 2bca40901f Change thickness and font of tab number 2016-02-08 21:34:10 -05:00
Anthony Restaino b81d9a0ed8 Change out arrow drawable for an icon that displays current number of tabs 2016-02-08 21:27:22 -05:00
Anthony Restaino 79d619f82b Support bookmark importing from chrome variants and stock browser, lint fixes 2016-02-07 12:34:04 -05:00
Anthony Restaino c684472f6e Add new tab button to desktop tabs view 2016-02-06 22:27:08 -05:00
Anthony Restaino 7f4cab1e2e Add accessors to get Chrome dev and beta bookmarks, also fix potential bugs 2016-02-06 22:06:24 -05:00
Anthony Restaino c4e5553785 Workaround for travis not having enough memory :-/ guess we'll just build debug versions 2016-02-05 22:27:41 -05:00
Anthony Restaino 000ecbdc25 Allow close dialog to be shown when tabs icon is pressed 2016-02-05 22:23:43 -05:00
Anthony Restaino f1467a9a96 Add ability to close all tabs except current tab 2016-02-05 22:17:15 -05:00
Anthony Restaino 941f54d615 Renamed OnSubscribe/Subscriber, moved anonymous class to static class, added --stacktrace to gradle build to capture build crash 2016-02-05 21:54:45 -05:00
Anthony Restaino 77465c83dd Reactive code for reading activity 2016-02-04 23:59:01 -05:00
Anthony Restaino d861a9a502 Add support for onStart and onError 2016-02-04 20:35:09 -05:00
Anthony Restaino c05cc7c9be Fix build problem, make class static and fix some generics problems 2016-02-03 19:58:05 -05:00
Anthony Restaino ac3f43a76f Prevent observers from sending events out of order, add documentation, annotations 2016-02-02 22:06:28 -05:00
Anthony Restaino 84627b3fae Show the last created tab after initialization 2016-02-01 22:38:40 -05:00
Anthony Restaino de4fdc86e0 Add missing annotations, clean up reactive code, simplify methods 2016-02-01 22:32:12 -05:00
Anthony Restaino c4921bbf20 Added missing annotations to react 2016-02-01 22:23:16 -05:00
Anthony Restaino e2d46bdae2 Fixed StrictMode problems, created a reactive implementation class, fixed potential NPEs, fixed memory leak
* Fixed places where IO was done on main thread
* Created reactive class Observable so that work could easily be done on
other threads
* Fixed potential NPEs in LightningView
* Fixed memory leak where ConnectivityManager was leaking activity
2016-02-01 22:17:44 -05:00
Anthony Restaino ba3edc00e8 get rid of listener between tab manager and presenter. invert the dependency between them. 2016-01-31 21:01:13 -05:00
Anthony Restaino 965c5f565f mostly move delete tab and new tab and handle new intent to presenter 2016-01-31 20:18:27 -05:00
Anthony Restaino 4a21d3f4f9 Use Executor thread pool instead of creating my own threads on the fly 2016-01-30 22:46:57 -05:00
Anthony Restaino 135cf2e572 Lint fixes, change nullable annotation in preference manager 2016-01-30 22:11:45 -05:00
Anthony Restaino 65c2c9c461 Initial slow move toward MVP pattern 2016-01-29 22:33:01 -05:00
Anthony Restaino 0e211ebf85 Add missing annotation 2016-01-28 21:19:44 -05:00
Anthony Restaino 359a252f24 Null annotations for rest of classes 2016-01-28 21:18:39 -05:00
Anthony Restaino 970ffbaca8 Add null annotations for fragments 2016-01-28 21:16:25 -05:00
Anthony Restaino b82d304d7f Fix nullable problem in ThemeUtils 2016-01-27 23:45:15 -05:00
Anthony Restaino 17e2640248 Fix null annotations, issues in various classes 2016-01-27 23:42:48 -05:00
Anthony Restaino 9cf0a7e11e Annotate networkreceiver 2016-01-27 23:27:22 -05:00
Anthony Restaino 12c2ada750 Add missing annotation 2016-01-27 23:26:43 -05:00
Anthony Restaino ff3d94635a Fix null issues with bitmap 2016-01-27 23:25:34 -05:00
Anthony Restaino 8f38b91dc1 Non null annotations in LightningWebClient/ChromeClient 2016-01-27 23:18:21 -05:00
Anthony Restaino 4eb292f40f Infer nullity 2016-01-27 20:49:27 -05:00
Anthony Restaino dcd042b9d5 Annotate method parameters, lint fixes 2016-01-26 20:32:35 -05:00
Anthony Restaino 04e0d5650f Add missing method documentation 2016-01-25 20:46:45 -05:00
Mark b93413f9a3 Update
Translated two new strings
2016-01-25 21:01:56 +01:00
Anthony Restaino 416dc4594d add ability to add shortcuts to the homescreen, more work still needed 2016-01-24 17:39:09 -05:00
Anthony Restaino c19dbe09bb Cleanup unused methods 2016-01-24 17:00:46 -05:00
Anthony Restaino 425392456c Documentation, cleanup of TabsManager 2016-01-24 15:32:16 -05:00
Anthony Restaino 29836bd98a Save tab back/forward state, not just current site when saving/restoring state 2016-01-24 11:02:56 -05:00
Anthony Restaino f73f82030f Use Application object instead of explicit Context 2016-01-24 00:20:31 -05:00
Anthony Restaino 600034b6fa Remove unused imports 2016-01-24 00:14:14 -05:00
Anthony Restaino ac107d6704 Variable renaming, moving fields around, more injection, move ProxyUtils out of flavor specific code 2016-01-23 19:55:11 -05:00
Anthony Restaino cb52aa0065 Inject Bus, HistoryDatabase, and PreferenceManager rather than using BrowserApp to access instances 2016-01-23 19:36:05 -05:00
Anthony Restaino db52a94d8c Remove static context getter from BrowserApp 2016-01-23 12:54:57 -05:00
Anthony Restaino 076b74e867 Add missing changes for history page changes 2016-01-23 12:53:39 -05:00
Anthony Restaino f6b60894f6 Make HistoryPage an AsyncTask to be easier to use 2016-01-23 12:53:19 -05:00
Anthony Restaino 24385c4334 Make StartPage an AsyncTask, makes it simpler to use 2016-01-23 12:39:21 -05:00
Anthony Restaino 22960c9bd6 Make BookmarkPage an AsyncTask to simplify its use, change recursion to iteration in DownloadHandler 2016-01-23 12:27:58 -05:00
Anthony Restaino 930880b339 Remove more uses of the static context from BrowserApp 2016-01-22 23:27:26 -05:00
Anthony Restaino a434c0af68 Utilize IconCacheTask, add Application.get because maybe storing the application context is bad???? 2016-01-22 23:00:32 -05:00
Anthony Restaino c95f1f86e9 No need to inject BookmarkPage, make it a utility class for right now 2016-01-22 22:39:47 -05:00
Anthony Restaino 68a4475ec7 Fixed Lite build error 2016-01-22 08:19:45 -05:00
Anthony Restaino da4985d4de Fixed crash when opening browser from intent 2016-01-22 08:19:33 -05:00
Anthony Restaino 8b44ce12fa Dependency injection for ReadingActivity 2016-01-21 20:42:19 -05:00
Anthony Restaino 6084c9b478 Make ProxyUtils a proper dagger singleton, inject more member variables where possible 2016-01-21 20:35:00 -05:00
Anthony Restaino a24eb45ae4 Properly use AppComponent to only inject classes into dagger, inject static dependencies into BrowserApp class 2016-01-21 20:16:01 -05:00
Anthony Restaino a60ae614d9 Protect incognito activity from intents, clean up some code analysis warnings, simplify LightningView settings methods 2016-01-20 22:02:15 -05:00
Anthony Restaino 46b1269730 Correctly remove WebView from layout before destroying it. throw exception if destroy is called without remove 2016-01-19 21:48:20 -05:00
Anthony Restaino ee52e00c83 Fixed memory leak caused by incorrectly destroying the WebView before it was removed from its parent 2016-01-18 21:39:09 -05:00
Anthony Restaino 5368d76218 Documentation for Utils class 2016-01-17 23:45:12 -05:00
Anthony Restaino 35855a1c02 Revert change to leak canary version, add todo for bug 2016-01-16 22:49:31 -05:00
Anthony Restaino c1083f6aab Fix lint issues with color ints 2016-01-16 22:39:26 -05:00
Anthony Restaino 3d745cbe6e Update to stable leak canary release 2016-01-16 22:39:10 -05:00
Anthony Restaino 0185b5c1ba Documentation for LightningViewTitle. 2016-01-15 21:49:12 -05:00
Anthony Restaino 25ff01ed79 Comment formatting 2016-01-15 21:43:55 -05:00
Anthony Restaino 6aaee4ce48 Finish LightningView documentation, add nullable/nonnull annotations to some methods 2016-01-15 21:41:48 -05:00
Anthony Restaino ae15c9c816 start documentation in LightningView, remove direct field access and replace with getters 2016-01-14 21:16:36 -05:00
Anthony Restaino 09679571d7 Attempt to fix build error by pulling latest tools 2016-01-13 18:46:27 -05:00
Anthony Restaino 34327136d9 Fixed CI build error due to missing build tools 2016-01-12 20:25:36 -05:00
Anthony Restaino 290e77e696 added content URIs for Chrome dev and beta and debug methods 2016-01-11 22:50:00 -05:00
Anthony Restaino edbd95418c Potential javascript to extract theme meta tag (currently no-op) 2016-01-11 22:49:33 -05:00
Anthony Restaino 27e01483b1 Update gradle dependencies, fix a number of lint errors
Note: resource closed inspections that were ignored were ignored because
they were being properly closed in finally{} blocks
2016-01-11 22:26:32 -05:00
Anthony Restaino 9b56d92922 Fixed broken gradle build 2016-01-11 19:34:21 -05:00
Anthony Restaino 7318a818c4 Async loading of homepage, delegate IOThread responsibility to BrowserApp class 2016-01-10 22:34:02 -05:00
Anthony Restaino e06d530528 Run UI operations on correct thread 2016-01-10 19:31:36 -05:00
Anthony Restaino 057b4296d7 Mirror AppComponent getters in BrowserApp so that classes are less reliant on AppComponent, refactored getAppContext to getContext 2016-01-10 15:05:23 -05:00
Anthony Restaino f00bb77851 Start using a single thread executor for any database access to eliminate unnecessary thread creation 2016-01-10 14:45:03 -05:00
Anthony Restaino cb19ce2d0a Fixed memory leak in IncognitoActivity 2016-01-10 14:02:13 -05:00
Anthony Restaino 8d390e1d6d Update gradle to use LeakCanary snapshot so that leak detection works on marshmallow 2016-01-10 13:55:19 -05:00
Nesswit 2e55ceba0c Add increase contrast filter 2016-01-05 01:22:35 +09:00
Anthony Restaino 95dddf1992 Merge pull request #351 from M2ck/patch-1
updated french translation
2015-12-13 12:14:43 -05:00
Anthony Restaino f56631708e Merge pull request #350 from takahirom/master
Add japanese translation.
2015-12-13 12:14:29 -05:00
M2ck 5e4ec63c32 french translation up to date 2015-12-05 15:55:33 +01:00
M2ck c3ec66d3a2 [WIP] updated french translation 2015-12-04 22:54:05 +01:00
takahirom f48b71b390 Add japanese translation 2015-12-01 00:31:23 +09:00
takahirom 39683c704e Add japanese translation 2015-12-01 00:24:16 +09:00
Anthony Restaino 0a4d81f7e2 perform exit cleanup when browser is closed regardless of whether last tab is deleted or not 2015-11-22 22:17:36 -05:00
Anthony Restaino de2d0b2ca4 Fixed close tab behavior, fixed UI corner case bug 2015-11-22 22:06:15 -05:00
Anthony Restaino 8da11b4f08 Merge pull request #326 from cliqz-oss/dev
Tab deletion logic moved to TabsManager
2015-11-22 21:55:58 -05:00
Anthony Restaino 2a4b636a53 Fix bug with navigation drawer, update gradle dependencies 2015-11-21 18:21:58 -05:00
Anthony Restaino c5328c4e3d Merge branch 'dev' of https://github.com/anthonycr/Lightning-Browser into dev 2015-11-21 17:44:52 -05:00
Anthony Restaino 4f67fd8e94 Catch non 2xx responses and don't try to open an input stream 2015-11-21 17:44:45 -05:00
Anthony Restaino 9731250d27 Merge pull request #340 from MarkThat/patch-1
Translation of new and old strings.
2015-11-21 17:16:57 -05:00
Anthony Restaino 7354e354db Move language initialization to constructor 2015-11-21 11:48:03 -05:00
Anthony Restaino 171715f40c Update to search suggestions API that supports HTTPS 2015-11-21 11:03:10 -05:00
Mark 6ea15553de Translation of new and old strings. 2015-11-18 23:45:04 +01:00
Anthony Restaino 8b82ac5e51 Update hosts file with latest version from hosts-file.net 2015-11-05 21:58:37 -05:00
Anthony Restaino 47341ce927 Reformatted manifest file 2015-11-05 21:53:51 -05:00
Anthony Restaino c83a7d0058 Merge pull request #322 from pejakm/cccons
Add clear_cookies string to fix context inconsistencies
2015-11-05 21:49:17 -05:00
Anthony Restaino 734574616d Make the workaround more obvious 2015-11-05 21:47:14 -05:00
Anthony Restaino cb98ee783b Workaround for bug in the appcompat support library 2015-11-05 21:18:28 -05:00
Mladen Pejaković 3a95aea82f Fix merge conflict 2015-11-05 08:21:18 +01:00
Anthony Restaino 79b8253b21 Merge branch 'dev' of https://github.com/anthonycr/Lightning-Browser into dev 2015-11-04 23:33:56 -05:00
Anthony Restaino 1eeddaf502 Fix crash that could occur pre API 16 2015-11-04 23:33:45 -05:00
Anthony Restaino aa9a7123b2 Merge pull request #327 from kuc/migrate-to-https
Migrate to HTTPS
2015-11-04 22:02:56 -05:00
Stefano Pacifici 6f914e9e17 Better handling of bookmarks, some responsability moved back to BrowserActivity 2015-11-04 14:21:44 +01:00
Miłosz Sieradzki fd7cc30470 Fix checks to allow both HTTP and HTTPS URLs 2015-11-03 22:28:54 +01:00
Miłosz Sieradzki 5059a3d01b Fix methods from SHelper
Both Google and Facebook force HTTPS-only traffic for years.
2015-11-03 22:28:07 +01:00
Miłosz Sieradzki bfc6c3dadc Migrate all trafic to Google services to HTTPS 2015-11-03 22:21:19 +01:00
Stefano Pacifici 63f2c5f798 Merge pull request #2 from ravjit-cliqz/dev
Changed the scope of removeTab to private
2015-11-03 16:11:26 +01:00
Ravjit Singh Uppal cc75ba1bc7 Changed the scope of removeTab to private 2015-11-03 15:45:44 +01:00
Stefano Pacifici 1eb2407543 Merge pull request #1 from ravjit-cliqz/dev
Moved deleting logic to tabs manager
2015-11-03 15:41:35 +01:00
Ravjit Singh Uppal 006eb5e191 moved the deleting logic to TabsManager 2015-11-03 15:37:56 +01:00
Anthony Restaino 9a9a06fe7b Add support for multiple languages in search suggestions 2015-11-01 17:25:40 -05:00
Anthony Restaino 23dc83fb6a Fixed bug where you could add generated html pages as bookmarks 2015-11-01 16:00:28 -05:00
Anthony Restaino d66f5e4c17 Remove headers in case the setting is disabled after being enabled 2015-10-30 23:43:25 -04:00
Anthony Restaino 6df7cdf331 Corrected variable names. 2015-10-30 23:38:14 -04:00
Anthony Restaino 7a0c79d11e Add support to remove identifying headers, add support for DNT header requests 2015-10-30 23:33:35 -04:00
Anthony Restaino 4e3193bfc8 Fix bug where you couldn't turn flash on on supported devices 2015-10-30 20:43:15 -04:00
Anthony Restaino 5dfc948fd3 Fix issue where warning dialog was not shown for local files in some cases 2015-10-30 20:14:01 -04:00
Anthony Restaino 80ac1928c1 Fixed bug where the homepage file url was showing 2015-10-29 23:35:58 -04:00
Anthony Restaino 441b189fad Merge pull request #321 from pejakm/srupd
Update Serbian
2015-10-29 19:07:05 -04:00
Anthony Restaino dc188c54e3 Merge pull request #319 from ByteHamster/dev
Workaround for #270
2015-10-29 19:05:44 -04:00
Mladen Pejaković 3597f7f812 Add clear_cookies string to fix context inconsistencies 2015-10-26 20:18:28 +01:00
Mladen Pejaković 2abf75b669 Update Serbian 2015-10-26 19:59:41 +01:00
Anthony Restaino f2aa6d6e5c Properly destroy WebView 2015-10-24 14:32:39 -04:00
ByteHamster 32d36f3687 Disabled scaling on bookmarks page 2015-10-22 22:15:10 +02:00
ByteHamster 8169294c80 Workaround for #270
In my opinion, it is neccessary for a browser to open local files.
Because local files might be a security risk,
ask the user before opening a local file.
2015-10-22 22:11:34 +02:00
Anthony Restaino 7aaf6d1771 Fixed memory leak 2015-10-21 22:45:20 -04:00
Anthony Restaino 34312bb988 Switch to grant library for permissions handling 2015-10-21 21:42:22 -04:00
Anthony Restaino 94b69fd328 Update to latest support libraries, move permissions stuff to separate package 2015-10-18 15:15:36 -04:00
Anthony Restaino fd8cfb7031 Mistakenly removed build tools 22 that netcipher relies on 2015-10-17 23:13:37 -04:00
Anthony Restaino fc93858918 Switched to correct build tools 2015-10-17 23:09:59 -04:00
Anthony Restaino c0ce7e74bd Tryin 2 fix travis ci. local builds aren't failing with lint errors :( 2015-10-17 23:05:33 -04:00
Anthony Restaino 13c6594e0c Removed redundant character escapes to fix lint errors. 2015-10-17 22:12:07 -04:00
Anthony Restaino bf4c90b121 Fixed bugs in showTab, attempt to improve full-screen video handling. 2015-10-17 21:50:52 -04:00
Anthony Restaino 9f755aeed7 Fixed bug where opening a URL in the browser wouldn't work, refactored the ui controller, fixed bad database practices. 2015-10-17 13:59:51 -04:00
Anthony Restaino e707e338ef Fixed new bug where browser wouldn't close on new intent. Fixed potential vuln in downloading code. Formatted some code. 2015-10-15 23:23:04 -04:00
Anthony Restaino 7bba86d963 Fixed recently introduced UI bug in desktop tab mode. 2015-10-15 22:45:56 -04:00
Anthony Restaino 577efb76a4 Fixed security vulnerability in the intent selector 2015-10-15 22:11:24 -04:00
Anthony Restaino 1c96b62eb6 Add back SSL error detection that was removed, fixed static analysis warnings. 2015-10-15 21:45:54 -04:00
Anthony Restaino 72ee377a35 Fixed more bugs recently introduced. Hardened asynctasks against memory leaks. Fixed some other stuff 2015-10-15 20:24:04 -04:00
Anthony Restaino 88549bf156 Fixed number of UI bugs recently introduced in Tabs changes merge from S. Pacifici 2015-10-14 23:58:47 -04:00
Anthony Restaino ce0e02585c Document the PermissionsManager 2015-10-14 23:23:04 -04:00
Anthony Restaino 99e4773e45 Preliminary fix for permissions, fixed a new crash, formatted some code 2015-10-14 22:55:39 -04:00
Anthony Restaino 159053841a Add dex counter, fixed new bugs in bookmarks, fixed bug in bookmark sync, todo fix downloading bug 2015-10-14 21:21:51 -04:00
Anthony Restaino 5d55e480c9 Merge pull request #296 from stefano-cliqz/experimental_tabs
Experimental tabs
2015-10-09 19:47:50 -04:00
Stefano Pacifici 367f2a09d7 Merge branch 'dev' of github.com:anthonycr/Lightning-Browser into experimental_tabs 2015-10-09 14:55:27 +02:00
Stefano Pacifici a3f3fbd401 Improving the #296 pull request 2015-10-09 12:36:08 +02:00
Anthony Restaino d3867d29bd Be more clear on code style 2015-10-08 20:08:15 -04:00
Anthony Restaino c04cff510b Merge branch 'master' into dev 2015-10-07 22:07:54 -04:00
Anthony Restaino d7017789f6 Merge branch 'dev' of https://github.com/anthonycr/Lightning-Browser into dev 2015-10-07 22:07:29 -04:00
Anthony Restaino 3c51870486 Removed commented out line that was being compiled...
seriously, every time I compiled the free version this line got compiled
as if it wasn't commented out. Regardless, it doesn't need to be there.
2015-10-07 22:07:22 -04:00
Anthony Restaino f3a9a9a46d Merge pull request #307 from ByteHamster/dev
Added 'home' button to tab drawer, German translation updates
2015-10-07 21:51:51 -04:00
ByteHamster 741d389da4 Updated German translation 2015-10-06 19:25:49 +02:00
ByteHamster b8058ad345 Added 'home' button to tab drawer 2015-10-06 19:07:52 +02:00
Anthony Restaino 0672b87ec7 Added trello board info 2015-10-05 20:05:33 -04:00
Stefano Pacifici ab7273106f Merge branch 'dev' of github.com:anthonycr/Lightning-Browser into experimental_tabs 2015-10-05 17:56:12 +02:00
Anthony Restaino b3cec67313 Added more download options 2015-10-03 16:03:01 -04:00
Anthony Restaino 36860cc848 Merge pull request #303 from MarkThat/dev
Translated from scratch
2015-10-03 15:51:09 -04:00
Anthony Restaino 0c467d5bfe Merge pull request #302 from kuc/update-polish-translations
Update Polish translations
2015-10-03 15:50:21 -04:00
Mark. aacec74aba Translated from scratch
Some errors should be fixed now, i also added some strings which i did not translate before.
2015-10-02 22:35:24 +02:00
Miłosz Sieradzki 8cb4b455cf Update Polish translations 2015-10-02 22:03:22 +02:00
Anthony Restaino 5fa6b529ca Merge pull request #299 from anthonycr/dev
Version 4.2.3 Release
2015-09-30 22:43:21 -04:00
Anthony Restaino c352c331ad last changes for 4.2 update 2015-09-30 22:27:12 -04:00
Anthony Restaino c190066db2 Merge pull request #298 from kuc/fix-onReceivedSslError
Fixes #297: properly implement onReceivedSslError() method
2015-09-30 20:48:34 -04:00
Miłosz Sieradzki 06e80ad541 Fixes #297: properly implement onReceivedSslError() method
Validation of SSL certificates is still not ideal, as https://badssl.com/ shows, but further improvements require more investigation.
2015-09-30 21:56:14 +02:00
Stefano Pacifici 61b57cd992 Restore activity restart when tab mode changes 2015-09-29 14:39:05 +02:00
Anthony Restaino a015d810ea Fix UI bug caused by obfuscation 2015-09-29 07:42:56 -04:00
Stefano Pacifici 3cb576d358 Merge latest changes from Anthony's dev branch 2015-09-29 12:11:08 +02:00
Anthony Restaino f761383fc4 Up version number, fix build error. 2015-09-28 20:36:04 -04:00
Stefano Pacifici b0c1bcc028 iml files removed. They are generated during gradle sync by Android Studio. 2015-09-28 16:07:38 +02:00
Stefano Pacifici 1f025debd7 Solve problems with colors when tabs are switched 2015-09-28 15:44:23 +02:00
Anthony Restaino c67a1108cd Reduce visibility of members and methods where possible, and more (see description)
* reduce visibility
* remove unused methods and members
* Suppress unused warnings we can ignore
* fixed or ignored deprecation warnings
* Changed HistoryItem to have better hashcode and equals implementations
and removed id member from it as it was unnecessary
* Fix performance problem with loading bookmarksettingsfragment and
properly annotate bookmarklocalsync
2015-09-27 22:19:59 -04:00
Anthony Restaino 3bd08d00f3 Begin adding documentation, remove unnecessary controller method 2015-09-27 18:56:49 -04:00
Anthony Restaino 38d1973a93 Lint fixes, save scroll position in bookmarks list 2015-09-27 15:40:04 -04:00
Anthony Restaino 6bbc0805de Fixed bug where tab and toolbar colors were not in sync when color mode got switched on 2015-09-27 12:45:47 -04:00
Anthony Restaino e157d45d39 Use executorservice instead of plain executor to facilitate shutdown 2015-09-27 11:58:58 -04:00
Anthony Restaino 7cec3bd6e4 Add back importing from stock browser and an attempt to add import from chrome
import from the default built in browser, stock browser ususally, but
chrome on marshmallow and above.
2015-09-27 11:58:37 -04:00
Anthony Restaino a71a8c3493 Better asynchronous image loading for BookmarksFragment
Previous AsyncTask would throw a RejectedExecutionException if too many
AsyncTasks got spawned on the thread pool executor. The solution was to
create a custom Executor that properly executed the task and queue it if
necessary. Also switched to using weakreference for the view and set
timeouts on image loading so it can load faster.
2015-09-27 11:51:18 -04:00
Anthony Restaino f1da3c4147 Updated download handler 2015-09-26 17:56:52 -04:00
Anthony Restaino 42471026b3 Fixed bugs in downloading code, Added butterknife, Added back proxying to lite 2015-09-26 17:55:21 -04:00
Stefano Pacifici 6749ca39b8 Simplified LightningView with externalized XXXClients 2015-09-22 16:15:17 +02:00
Anthony Restaino 6f36410e87 Added support for downloading files to directories not lying in the directory returned by getExternalStorage
Useful for devices with both internal and external storage
2015-09-20 18:21:49 -04:00
Stefano Pacifici 3615018816 ClickHandler removed, avoid call loop duirng long press on a webview between BrowserActivity and LightningView 2015-09-17 11:26:34 +02:00
Stefano Pacifici 030b839aa6 Trying to remove BrowserController interface 2015-09-17 09:46:00 +02:00
Anthony Restaino b3f991e598 Change variables to project naming convention 2015-09-16 21:52:34 -04:00
Anthony Restaino 4f839e0866 Remove unused resources, make methods static 2015-09-16 21:52:11 -04:00
Anthony Restaino 05efb4eb72 Fixed bugs in the BookmarksFragment and BookmarkManager 2015-09-16 21:51:15 -04:00
Stefano Pacifici 2563e81f7a Bookmark page generation moved to LightningView to avoid call loop between BrowserActivity and LightningView through BrowserController 2015-09-16 17:42:20 +02:00
Stefano Pacifici 5c2cf07e20 PreferenceManager injected 2015-09-16 16:49:59 +02:00
Anthony Restaino 7f965b0829 Properly close I/O streams that were not being properly closed 2015-09-15 23:03:17 -04:00
Anthony Restaino 5c8fd41c6b Made inner classes static to discourage access within of enclosing class 2015-09-15 23:02:49 -04:00
Anthony Restaino b6b2a25dbe Reduce unnecessary public visibility on internally used variables 2015-09-15 23:01:55 -04:00
Anthony Restaino 9a2ed38440 Equalized padding on autocomplete layout 2015-09-15 23:00:34 -04:00
Stefano Pacifici 4be31553ad Back, Forward and Plus rewired 2015-09-15 16:10:34 +02:00
Stefano Pacifici 7661ea35ee In the middle of events rewiring (back/forward) 2015-09-15 14:24:31 +02:00
Anthony Restaino 748397f1f0 remove redundant calls to "showTab" 2015-09-14 20:18:24 -04:00
Anthony Restaino c65cccb25c Remove unnecessary ClickListener classes 2015-09-14 20:03:35 -04:00
Anthony Restaino 2da5c4194c Fixed static analysis warnings
* Using strings when characters could be used
* Unused imports
* String concatenation in a loop
2015-09-14 20:03:11 -04:00
Anthony Restaino 8a6ad81027 Extract anonymous caching class to its own inner class 2015-09-14 19:58:46 -04:00
Stefano Pacifici 51f783cea4 TabsFragment extracted 2015-09-14 17:58:21 +02:00
Stefano Pacifici 74073178bf mWebView reference removed from BrowserActivity 2015-09-14 14:41:11 +02:00
Stefano Pacifici f0c3b743d4 CurrentTab reference removed from BrowserActivity 2015-09-14 14:19:07 +02:00
Stefano Pacifici 74a75d4adb TabsManager created 2015-09-14 13:44:36 +02:00
Stefano Pacifici 5628433718 iml files removed. They are generated during gradle sync by Android Studio. 2015-09-14 10:15:34 +02:00
Anthony Restaino 919043cad9 Fixed bug in release builds where event bus events were not being fired 2015-09-13 13:16:23 -04:00
Anthony Restaino 0b94eda458 Initialize ui color variable 2015-09-12 11:10:51 -04:00
Anthony Restaino 57a25eb9dc Fixed ColorMode on the desktop tab UI by caching the backing Bitmap rather than immutable BitmapDrawable
BitmapDrawable turns out is sort of immutable even when using mutate()
so what was happening was that when switching from a tab on the right to
a tab on the left, the foreground drawable was set as the background of
two views for a small instant as the RecyclerView binds views from left
to right and the setColorFilter on the left foreground tab was not
working at all. When you switched from a left to right tab, it worked
fine because the left tab background was changed before the right and
the foreground drawable was only used by one view in that case. The
solution was to not reuse the drawable but instead reuse the backing
bitmap and create a new drawable whenever a tab moved to the foreground.
2015-09-12 10:36:09 -04:00
Anthony Restaino 965ccee8b7 Update to faster jsoup library version 2015-09-11 22:14:26 -04:00
Anthony Restaino 5fd401c2c0 Use thread pool executors on AsyncTasks to increase performance 2015-09-11 20:28:01 -04:00
Anthony Restaino 161f4100b3 Cache icons on a background thread 2015-09-11 20:27:30 -04:00
Anthony Restaino 875cd45c7b Updated to latest Google logo 2015-09-11 20:26:07 -04:00
Anthony Restaino 0ac2337ff8 Refactored ProxyUtils for lite version 2015-09-10 08:04:40 -04:00
Anthony Restaino 838270b4b0 Fix broken icon downloading, handle edge cases where url parameter is bad 2015-09-09 23:40:54 -04:00
Anthony Restaino 3fab58955c Removed need for passing a Context to the ProxyUtils singleton 2015-09-09 22:18:20 -04:00
Anthony Restaino dbf0457d79 Don't clear the HashMap, instead just change the reference 2015-09-09 21:32:05 -04:00
Anthony Restaino 5dff2db5df Add LeakCanary library, fix a few memory leaks 2015-09-08 22:24:15 -04:00
Anthony Restaino d5102b5e54 Fixed a number of lint warnings 2015-09-08 21:10:34 -04:00
Anthony Restaino 7f07edcdf7 Fixed compile bug in LightningLite 2015-09-08 20:50:17 -04:00
Anthony Restaino b33c4caf67 Fixed bug with WebView background being transparent, fixed some deprecated API usage, made HistoryDatabase a true singleton 2015-09-08 20:48:08 -04:00
Anthony Restaino 681a76df50 formatting change in browseractivity 2015-09-07 20:42:33 -04:00
Anthony Restaino e00c82655a Remove pointless assertions: @ NonNull removes need for assertions 2015-09-07 20:34:06 -04:00
Anthony Restaino 732d309888 Cleaning up lint warnings and making some performance improvements on string builders 2015-09-07 20:31:59 -04:00
Anthony Restaino 3b75765d92 Add a transition when entering and exiting the Reading mode 2015-09-07 20:01:14 -04:00
Anthony Restaino b0169e73d2 Use support library DrawerArrowDrawable instead of using our own version 2015-09-07 20:01:12 -04:00
Anthony Restaino 71d6da0eee Lint fixes, remove use of assert from code, update to latest support library 2015-09-07 20:01:11 -04:00
Anthony Restaino 1b0b256ce8 Update README.md 2015-09-07 19:56:33 -04:00
Anthony Restaino dcc67fbdb6 Merge pull request #284 from stefano-cliqz/dev
Refactoring: Bookmarks as Fragment
2015-09-07 15:37:08 -04:00
Stefano Pacifici 2619210f8c Fix removing the BookmarksEvent.Deleted instead of the actual bookmark 2015-09-07 10:02:23 +02:00
Stefano Pacifici 83790bec70 Fix bookmarks drawer background problems 2015-09-03 15:57:12 +02:00
Stefano Pacifici 23e97306dd BookmarkPage restored and proper dependency injection 2015-09-03 15:33:40 +02:00
Stefano Pacifici 47103ba3d0 Activity Transaction animations merged 2015-09-02 15:24:33 +02:00
Stefano Pacifici 4eaf01e6cc Updated netchiper submodule 2015-08-27 22:00:31 +02:00
Stefano Pacifici 3c9cd73bf0 Refactoring: Bookmarks as Fragment
1. Incognito mode in another process
2. Bookmarks as a Fragement using Otto
3. Initial bookmarks as fragment implementation
2015-08-27 16:50:36 +02:00
147 arquivos alterados com 14210 adições e 5105 exclusões
+2
Ver Arquivo
@@ -50,3 +50,5 @@ out
# Source:
# https://raw.githubusercontent.com/github/gitignore/master/Android.gitignore
# https://gitlab.com/fdroid/fdroidclient/raw/master/.gitignore
*.iml
+3 -1
Ver Arquivo
@@ -2,8 +2,9 @@ language: android
sudo: false
android:
components:
- tools
- build-tools-23.0.3
- build-tools-22.0.1
- build-tools-23.0.0
- android-23
- android-22
- extra-android-support
@@ -17,3 +18,4 @@ before_install:
install:
- ./gradlew
script:
- ./gradlew assembleDebug --stacktrace
+12 -8
Ver Arquivo
@@ -4,7 +4,11 @@
####Download
* [Download APK from here](https://github.com/anthonycr/Lightning-Browser/releases)
* [Download from Google Play](https://play.google.com/store/apps/details?id=acr.browser.barebones)
* [Download from F-Droid](https://f-droid.org/repository/browse/?fdfilter=lightning&fdid=acr.browser.lightning)
* [Download Free from Google Play](https://play.google.com/store/apps/details?id=acr.browser.barebones)
* [Download Paid from Google Play](https://play.google.com/store/apps/details?id=acr.browser.lightning)
####Master Branch
* [![Build Status](https://travis-ci.org/anthonycr/Lightning-Browser.svg?branch=master)](https://travis-ci.org/anthonycr/Lightning-Browser)
@@ -21,15 +25,13 @@
* Incognito mode
* Flash support (prior to 4.4)
* Follows Google design guidelines
* Unique utilization of navigation drawer for tabs
* Google search suggestions
* Orbot Proxy support
* Orbot Proxy support and I2P support
####Permissions
@@ -41,10 +43,6 @@
* ````ACCESS_FINE_LOCATION````: For sites like Google Maps, it is disabled by default in settings and displays a pop-up asking if a site may use your location when it is enabled
* ````READ_HISTORY_BOOKMARKS````: To synchronize history and bookmarks between the stock browser and Lightning
* ````WRITE_HISTORY_BOOKMARKS````: To synchronize history and bookmarks between the stock browser and Lightning
* ````ACCESS_NETWORK_STATE````: Required for the WebView to function by some OEM versions of WebKit
####The Code
@@ -52,9 +50,15 @@
* Please add translations/translation fixes as you see need
####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
* PLEASE use the ````dev```` branch when contributing as the ````master```` branch is supposed to be for stable builds. I will not reject your pull request if you make it on master, but it will annoy me and make my life harder.
* Code Style
* Hungarian Notation
* Prefix member variables with 'm'
* 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):
-128
Ver Arquivo
@@ -1,128 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="Lightning-Browser" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":app" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="lightningPlusDebug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleLightningPlusDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileLightningPlusDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleLightningPlusDebugAndroidTest" />
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileLightningPlusDebugAndroidTestSources" />
<afterSyncTasks>
<task>generateLightningPlusDebugAndroidTestSources</task>
<task>generateLightningPlusDebugSources</task>
</afterSyncTasks>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/lightningPlus/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/lightningPlus/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/lightningPlus/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/lightningPlus/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/lightningPlus/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/lightningPlus/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/lightningPlus/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/lightningPlus/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/lightningPlusDebug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/lightningPlusDebug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/lightningPlusDebug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/lightningPlusDebug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/lightningPlusDebug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/lightningPlusDebug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/lightningPlusDebug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/lightningPlus/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/lightningPlus/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/lightningPlus/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/lightningPlus/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/lightningPlus/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/lightningPlus/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/LightningPlus/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/LightningPlus/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/LightningPlus/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/LightningPlus/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/LightningPlus/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/LightningPlus/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/LightningPlus/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestLightningPlus/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestLightningPlus/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestLightningPlus/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestLightningPlus/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestLightningPlus/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestLightningPlus/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestLightningPlus/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/appcompat-v7/23.0.0/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/design/23.0.0/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/palette-v7/23.0.0/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/recyclerview-v7/23.0.0/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/23.0.0/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/net.i2p.android/client/0.7/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="client-0.7" level="project" />
<orderEntry type="library" exported="" name="design-23.0.0" level="project" />
<orderEntry type="library" exported="" name="palette-v7-23.0.0" level="project" />
<orderEntry type="library" exported="" name="appcompat-v7-23.0.0" level="project" />
<orderEntry type="library" exported="" name="jsoup-1.8.1" level="project" />
<orderEntry type="library" exported="" name="recyclerview-v7-23.0.0" level="project" />
<orderEntry type="library" exported="" name="support-v4-23.0.0" level="project" />
<orderEntry type="library" exported="" name="support-annotations-23.0.0" level="project" />
<orderEntry type="module" module-name="libnetcipher" exported="" />
</component>
</module>
+64 -13
Ver Arquivo
@@ -1,17 +1,27 @@
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
apply plugin: 'com.getkeepsafe.dexcount'
android {
compileSdkVersion 23
buildToolsVersion "23.0.0"
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 14
targetSdkVersion 23
versionName "4.1.1a"
versionName "4.3.3"
generatedDensities = []
}
aaptOptions {
additionalParameters "--no-version-vectors"
}
sourceSets {
lightningPlus.setRoot('src/LightningPlus')
lightningLite.setRoot('src/LightningLite')
}
buildTypes {
debug {
minifyEnabled false
@@ -25,32 +35,73 @@ android {
proguardFiles 'proguard-project.txt'
}
}
productFlavors {
lightningPlus {
buildConfigField "boolean", "FULL_VERSION", "true"
applicationId "acr.browser.lightning"
versionCode 80
versionCode 88
}
lightningLite {
buildConfigField "boolean", "FULL_VERSION", "false"
applicationId "acr.browser.barebones"
versionCode 81
versionCode 90
}
}
lintOptions {
abortOnError false
abortOnError true
}
packagingOptions {
exclude '.readme'
}
}
dexcount {
includeClasses = false
includeFieldCount = false
printAsTree = true
orderByMethodCount = true
verbose = false
}
dependencies {
compile 'com.android.support:palette-v7:23.0.0'
compile 'com.android.support:appcompat-v7:23.0.0'
compile 'com.android.support:design:23.0.0'
compile 'com.android.support:recyclerview-v7:23.0.0'
compile 'org.jsoup:jsoup:1.8.1'
// Only Lightning Plus needs the proxy libraries
lightningPlusCompile 'net.i2p.android:client:0.7'
lightningPlusCompile(project(':libnetcipher'))
// support libraries
compile 'com.android.support:palette-v7:23.4.0'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'com.android.support:recyclerview-v7:23.4.0'
compile 'com.android.support:support-v4:23.4.0'
// html parsing for reading mode
compile 'org.jsoup:jsoup:1.9.2'
// event bus
compile 'com.squareup:otto:1.3.8'
// dependency injection
compile 'com.google.dagger:dagger:2.0.2'
apt 'com.google.dagger:dagger-compiler:2.0.2'
provided 'javax.annotation:jsr250-api:1.0'
// view binding
compile 'com.jakewharton:butterknife:7.0.1'
// permissions
compile 'com.anthonycr.grant:permissions:1.1.2'
// 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')
// memory leak analysis
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
}
+18
Ver Arquivo
@@ -37,6 +37,12 @@
-keep public class acr.browser.lightning.reading.*
-keep class org.lucasr.twowayview.** { *; }
-keepattributes *Annotation*
-keepclassmembers class ** {
@com.squareup.otto.Subscribe public *;
@com.squareup.otto.Produce public *;
}
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
@@ -44,6 +50,18 @@
public static *** i(...);
}
-keep class butterknife.** { *; }
-dontwarn butterknife.internal.**
-keep class **$$ViewBinder { *; }
-keepclasseswithmembernames class * {
@butterknife.* <fields>;
}
-keepclasseswithmembernames class * {
@butterknife.* <methods>;
}
# this will fix a force close in ReadingActivity
-keep public class org.jsoup.** {
public *;
@@ -1,58 +0,0 @@
package acr.browser.lightning.utils;
import android.app.Activity;
import android.content.Context;
/**
* 6/4/2015 Anthony Restaino
*/
public class ProxyUtils {
private static ProxyUtils mInstance;
private ProxyUtils(Context context) {
}
public static ProxyUtils getInstance(Context context) {
if (mInstance == null) {
mInstance = new ProxyUtils(context);
}
return mInstance;
}
/*
* If Orbot/Tor or I2P is installed, prompt the user if they want to enable
* proxying for this session
*/
public void checkForProxy(final Activity activity) {
}
/*
* Initialize WebKit Proxying
*/
private void initializeProxy(Activity activity) {
}
public boolean isProxyReady(Context context) {
return true;
}
public void updateProxySettings(Activity activity) {
}
public void onStop() {
}
public void onStart(final Activity activity) {
}
public int setProxyChoice(int choice, Activity activity) {
return choice;
}
}
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
+77 -57
Ver Arquivo
@@ -1,106 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2014 A.C.R. Development -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="acr.browser.lightning" >
<manifest package="acr.browser.lightning"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.android.browser.permission.READ_HISTORY_BOOKMARKS"/>
<uses-permission android:name="com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"/>
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
android:required="false"/>
<uses-feature
android:name="android.hardware.location"
android:required="false" />
android:required="false"/>
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
android:required="false"/>
<application
android:name=".activity.BrowserApp"
android:name=".app.BrowserApp"
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" >
android:label="@string/app_name">
<activity
android:name=".activity.MainActivity"
android:alwaysRetainTaskState="true"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/Theme.LightTheme" >
android:theme="@style/Theme.LightTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.APP_BROWSER" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.APP_BROWSER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="about" />
<data android:scheme="javascript" />
<data android:scheme="file"/>
<data android:mimeType="text/html"/>
<data android:mimeType="text/plain"/>
<data android:mimeType="application/xhtml+xml"/>
<data android:mimeType="application/vnd.wap.xhtml+xml"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="about"/>
<data android:scheme="javascript"/>
</intent-filter>
<!--
For these schemes where any of these particular MIME types
have been supplied, we are a good candidate.
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="inline" />
<data android:mimeType="text/html" />
<data android:mimeType="text/plain" />
<data android:mimeType="application/xhtml+xml" />
<data android:mimeType="application/vnd.wap.xhtml+xml" />
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="inline"/>
<data android:mimeType="text/html"/>
<data android:mimeType="text/plain"/>
<data android:mimeType="application/xhtml+xml"/>
<data android:mimeType="application/vnd.wap.xhtml+xml"/>
</intent-filter>
<!-- For viewing saved web archives. -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="file" />
<data android:mimeType="application/x-webarchive-xml" />
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="file"/>
<data android:mimeType="application/x-webarchive-xml"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.WEB_SEARCH" />
<action android:name="android.intent.action.WEB_SEARCH"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="http"/>
<data android:scheme="https"/>
</intent-filter>
<intent-filter>
<action android:name="info.guardianproject.panic.action.TRIGGER"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".activity.SettingsActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/settings"
android:theme="@style/Theme.SettingsTheme" >
android:theme="@style/Theme.SettingsTheme">
<intent-filter>
<action android:name="android.intent.action.SETTINGS" />
<action android:name="android.intent.action.SETTINGS"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
@@ -110,22 +130,22 @@
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/Theme.DarkTheme"
android:windowSoftInputMode="stateHidden" >
android:windowSoftInputMode="stateHidden|adjustResize">
<intent-filter>
<action android:name="android.intent.action.INCOGNITO" />
<action android:name="android.intent.action.INCOGNITO"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".activity.ReadingActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/reading_mode"
android:theme="@style/Theme.SettingsTheme" >
android:theme="@style/Theme.SettingsTheme">
<intent-filter>
<action android:name="android.intent.action.READING" />
<action android:name="android.intent.action.READING"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
</application>
Arquivo binário não exibido.

Antes

Largura:  |  Altura:  |  Tamanho: 14 KiB

Depois

Largura:  |  Altura:  |  Tamanho: 23 KiB

Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
@@ -1,19 +0,0 @@
package acr.browser.lightning.activity;
import android.app.Application;
import android.content.Context;
public class BrowserApp extends Application {
private static Context context;
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
}
public static Context getAppContext() {
return context;
}
}
@@ -2,28 +2,33 @@ package acr.browser.lightning.activity;
import android.content.Intent;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.Menu;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import acr.browser.lightning.R;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.react.Action;
import acr.browser.lightning.react.Observable;
import acr.browser.lightning.react.Subscriber;
@SuppressWarnings("deprecation")
public class IncognitoActivity extends BrowserActivity {
@Override
public void updateCookiePreference() {
CookieManager cookieManager = CookieManager.getInstance();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(this);
}
cookieManager.setAcceptCookie(PreferenceManager.getInstance().getIncognitoCookiesEnabled());
}
@Override
public synchronized void initializeTabs() {
newTab(null, true);
public Observable<Void> updateCookiePreference() {
return Observable.create(new Action<Void>() {
@Override
public void onSubscribe(@NonNull Subscriber<Void> subscriber) {
CookieManager cookieManager = CookieManager.getInstance();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(IncognitoActivity.this);
}
cookieManager.setAcceptCookie(mPreferences.getIncognitoCookiesEnabled());
subscriber.onComplete();
}
});
}
@Override
@@ -45,7 +50,7 @@ public class IncognitoActivity extends BrowserActivity {
}
@Override
public void updateHistory(String title, String url) {
public void updateHistory(@Nullable String title, @NonNull String url) {
// addItemToHistory(title, url);
}
@@ -56,7 +61,11 @@ public class IncognitoActivity extends BrowserActivity {
@Override
public void closeActivity() {
closeDrawers();
finish();
closeDrawers(new Runnable() {
@Override
public void run() {
closeBrowser();
}
});
}
}
@@ -2,29 +2,33 @@ package acr.browser.lightning.activity;
import android.content.Intent;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.Menu;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import acr.browser.lightning.R;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.react.Action;
import acr.browser.lightning.react.Observable;
import acr.browser.lightning.react.Subscriber;
@SuppressWarnings("deprecation")
public class MainActivity extends BrowserActivity {
@Override
public void updateCookiePreference() {
CookieManager cookieManager = CookieManager.getInstance();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(this);
}
cookieManager.setAcceptCookie(PreferenceManager.getInstance().getCookiesEnabled());
}
@Override
public synchronized void initializeTabs() {
restoreOrNewTab();
// if incognito mode use newTab(null, true); instead
public Observable<Void> updateCookiePreference() {
return Observable.create(new Action<Void>() {
@Override
public void onSubscribe(@NonNull Subscriber<Void> subscriber) {
CookieManager cookieManager = CookieManager.getInstance();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(MainActivity.this);
}
cookieManager.setAcceptCookie(mPreferences.getCookiesEnabled());
subscriber.onComplete();
}
});
}
@Override
@@ -35,8 +39,12 @@ public class MainActivity extends BrowserActivity {
@Override
protected void onNewIntent(Intent intent) {
handleNewIntent(intent);
super.onNewIntent(intent);
if (isPanicTrigger(intent)) {
panicClean();
} else {
handleNewIntent(intent);
super.onNewIntent(intent);
}
}
@Override
@@ -46,7 +54,7 @@ public class MainActivity extends BrowserActivity {
}
@Override
public void updateHistory(String title, String url) {
public void updateHistory(@Nullable String title, @NonNull String url) {
addItemToHistory(title, url);
}
@@ -57,7 +65,14 @@ public class MainActivity extends BrowserActivity {
@Override
public void closeActivity() {
closeDrawers();
moveTaskToBack(true);
closeDrawers(new Runnable() {
@Override
public void run() {
performExitCleanUp();
moveTaskToBack(true);
}
});
}
}
@@ -1,18 +1,19 @@
package acr.browser.lightning.activity;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.graphics.drawable.ColorDrawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@@ -21,26 +22,42 @@ import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.react.Action;
import acr.browser.lightning.react.Observable;
import acr.browser.lightning.react.OnSubscribe;
import acr.browser.lightning.react.Subscriber;
import acr.browser.lightning.react.Schedulers;
import acr.browser.lightning.react.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.ButterKnife;
public class ReadingActivity extends AppCompatActivity {
private TextView mTitle;
private TextView mBody;
private static final String TAG = ReadingActivity.class.getSimpleName();
@Bind(R.id.textViewTitle)
TextView mTitle;
@Bind(R.id.textViewBody)
TextView mBody;
@Inject PreferenceManager mPreferences;
private boolean mInvert;
private String mUrl = null;
private PreferenceManager mPreferences;
private int mTextSize;
private ProgressDialog mProgressDialog;
private Subscription mPageLoaderSubscription;
private static final float XXLARGE = 30.0f;
private static final float XLARGE = 26.0f;
@@ -51,7 +68,9 @@ public class ReadingActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
mPreferences = PreferenceManager.getInstance();
BrowserApp.getAppComponent().inject(this);
overridePendingTransition(R.anim.slide_in_from_right, R.anim.fade_out_scale);
mInvert = mPreferences.getInvertColors();
final int color;
if (mInvert) {
@@ -65,6 +84,7 @@ public class ReadingActivity extends AppCompatActivity {
}
super.onCreate(savedInstanceState);
setContentView(R.layout.reading_view);
ButterKnife.bind(this);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
@@ -72,9 +92,6 @@ public class ReadingActivity extends AppCompatActivity {
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mTitle = (TextView) findViewById(R.id.textViewTitle);
mBody = (TextView) findViewById(R.id.textViewBody);
mTextSize = mPreferences.getReadingTextSize();
mBody.setTextSize(getTextSize(mTextSize));
mTitle.setText(getString(R.string.untitled));
@@ -131,66 +148,87 @@ public class ReadingActivity extends AppCompatActivity {
}
if (getSupportActionBar() != null)
getSupportActionBar().setTitle(Utils.getDomainName(mUrl));
new PageLoader(this).execute(mUrl);
mPageLoaderSubscription = loadPage(mUrl).subscribeOn(Schedulers.worker())
.observeOn(Schedulers.main())
.subscribe(new OnSubscribe<ReaderInfo>() {
@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();
}
@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 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;
}
}
});
return true;
}
private class PageLoader extends AsyncTask<String, Void, Void> {
private final Activity mActivity;
private String mTitleText;
private String mBodyText;
public PageLoader(Activity activity) {
mActivity = activity;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
mProgressDialog = new ProgressDialog(mActivity);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
mProgressDialog.setCancelable(false);
mProgressDialog.setIndeterminate(true);
mProgressDialog.setMessage(mActivity.getString(R.string.loading));
mProgressDialog.show();
}
@Override
protected Void doInBackground(String... params) {
HtmlFetcher fetcher = new HtmlFetcher();
try {
JResult result = fetcher.fetchAndExtract(params[0], 2500, true);
mTitleText = result.getTitle();
mBodyText = result.getText();
} catch (Exception e) {
mTitleText = "";
mBodyText = "";
e.printStackTrace();
} catch (OutOfMemoryError e) {
System.gc();
mTitleText = "";
mBodyText = "";
e.printStackTrace();
private static Observable<ReaderInfo> loadPage(@NonNull final String url) {
return Observable.create(new Action<ReaderInfo>() {
@Override
public void onSubscribe(@NonNull Subscriber<ReaderInfo> subscriber) {
HtmlFetcher fetcher = new HtmlFetcher();
try {
JResult result = fetcher.fetchAndExtract(url, 2500, true);
subscriber.onNext(new ReaderInfo(result.getTitle(), result.getText()));
} catch (Exception e) {
subscriber.onError(new Throwable("Encountered exception"));
Log.e(TAG, "Error parsing page", e);
} catch (OutOfMemoryError e) {
System.gc();
subscriber.onError(new Throwable("Out of memory"));
Log.e(TAG, "Out of memory", e);
}
subscriber.onComplete();
}
return null;
});
}
private static class ReaderInfo {
@NonNull private final String mTitleText;
@NonNull private final String mBodyText;
public ReaderInfo(@NonNull String title, @NonNull String body) {
mTitleText = title;
mBodyText = body;
}
@Override
protected void onPostExecute(Void result) {
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
mProgressDialog = null;
}
if (mTitleText.isEmpty() || mBodyText.isEmpty()) {
setText(getString(R.string.untitled), getString(R.string.loading_failed));
} else {
setText(mTitleText, mBodyText);
}
super.onPostExecute(result);
@NonNull
public String getTitle() {
return mTitleText;
}
@NonNull
public String getBody() {
return mBodyText;
}
}
private void setText(String title, String body) {
@@ -221,6 +259,8 @@ public class ReadingActivity extends AppCompatActivity {
@Override
protected void onDestroy() {
mPageLoaderSubscription.unsubscribe();
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
mProgressDialog = null;
@@ -228,6 +268,14 @@ public class ReadingActivity extends AppCompatActivity {
super.onDestroy();
}
@Override
protected void onPause() {
super.onPause();
if (isFinishing()) {
overridePendingTransition(R.anim.fade_in_scale, R.anim.slide_out_to_right);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
@@ -3,6 +3,7 @@
*/
package acr.browser.lightning.activity;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.widget.Toolbar;
@@ -11,15 +12,18 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import com.anthonycr.grant.PermissionsManager;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import acr.browser.lightning.R;
import acr.browser.lightning.utils.PermissionsManager;
import acr.browser.lightning.app.BrowserApp;
public class SettingsActivity extends ThemableSettingsActivity {
private static final List<String> fragments = new ArrayList<>();
private static final List<String> mFragments = new ArrayList<>(7);
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -42,15 +46,30 @@ public class SettingsActivity extends ThemableSettingsActivity {
@Override
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.preferences_headers, target);
fragments.clear();
for (Header header : target) {
fragments.add(header.fragment);
mFragments.clear();
Iterator<Header> headerIterator = target.iterator();
while (headerIterator.hasNext()) {
Header header = headerIterator.next();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// Workaround for bug in the AppCompat support library
header.iconRes = R.drawable.empty;
}
if (header.titleRes == R.string.debug_title) {
if (BrowserApp.isRelease()) {
headerIterator.remove();
} else {
mFragments.add(header.fragment);
}
} else {
mFragments.add(header.fragment);
}
}
}
@Override
protected boolean isValidFragment(String fragmentName) {
return fragments.contains(fragmentName);
return mFragments.contains(fragmentName);
}
@Override
@@ -61,7 +80,7 @@ public class SettingsActivity extends ThemableSettingsActivity {
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
PermissionsManager.getInstance().notifyPermissionsChange(permissions);
PermissionsManager.getInstance().notifyPermissionsChange(permissions, grantResults);
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
@@ -0,0 +1,533 @@
package acr.browser.lightning.activity;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.webkit.WebView;
import com.squareup.otto.Bus;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.BookmarkPage;
import acr.browser.lightning.constant.Constants;
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.preference.PreferenceManager;
import acr.browser.lightning.react.Action;
import acr.browser.lightning.react.Observable;
import acr.browser.lightning.react.OnSubscribe;
import acr.browser.lightning.react.Schedulers;
import acr.browser.lightning.react.Subscriber;
import acr.browser.lightning.utils.FileUtils;
import acr.browser.lightning.utils.UrlUtils;
import acr.browser.lightning.view.LightningView;
/**
* A manager singleton that holds all the {@link LightningView}
* and tracks the current tab. It handles creation, deletion,
* restoration, state saving, and switching of tabs.
*/
public class TabsManager {
private static final String TAG = TabsManager.class.getSimpleName();
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<LightningView> mTabList = new ArrayList<>(1);
@Nullable private LightningView mCurrentTab;
@Nullable private TabNumberChangedListener mTabNumberListener;
private boolean mIsInitialized = false;
private final List<Runnable> mPostInitializationWorkList = new ArrayList<>();
@Inject PreferenceManager mPreferenceManager;
@Inject BookmarkManager mBookmarkManager;
@Inject HistoryDatabase mHistoryManager;
@Inject Bus mEventBus;
@Inject Application mApp;
public TabsManager() {
BrowserApp.getAppComponent().inject(this);
}
// TODO remove and make presenter call new tab methods so it always knows
@Deprecated
public interface TabNumberChangedListener {
void tabNumberChanged(int newNumber);
}
public void setTabNumberChangedListener(@Nullable TabNumberChangedListener listener) {
mTabNumberListener = listener;
}
public void cancelPendingWork() {
mPostInitializationWorkList.clear();
}
public void doAfterInitialization(@NonNull Runnable runnable) {
if (mIsInitialized) {
runnable.run();
} else {
mPostInitializationWorkList.add(runnable);
}
}
private void finishInitialization() {
mIsInitialized = true;
for (Runnable runnable : mPostInitializationWorkList) {
runnable.run();
}
}
/**
* Restores old tabs that were open before the browser
* was closed. Handles the intent used to open the browser.
*
* @param activity the activity needed to create tabs.
* @param intent the intent that started the browser activity.
* @param incognito whether or not we are in incognito mode.
*/
public synchronized Observable<Void> initializeTabs(@NonNull final Activity activity,
@Nullable final Intent intent,
final boolean incognito) {
return Observable.create(new Action<Void>() {
@Override
public void onSubscribe(@NonNull final Subscriber<Void> subscriber) {
// Make sure we start with a clean tab list
shutdown();
// If incognito, only create one tab, do not handle intent
// in order to protect user privacy
if (incognito) {
newTab(activity, null, true);
subscriber.onComplete();
return;
}
String url = null;
if (intent != null) {
url = intent.getDataString();
}
Log.d(TAG, "URL from intent: " + url);
mCurrentTab = null;
if (mPreferenceManager.getRestoreLostTabsEnabled()) {
restoreLostTabs(url, activity, subscriber);
} else {
newTab(activity, null, false);
finishInitialization();
subscriber.onComplete();
}
}
});
}
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<Bundle>() {
@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();
}
} 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);
builder.setCancelable(true)
.setTitle(R.string.title_warning)
.setMessage(R.string.message_blocked_local)
.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();
} else {
newTab(activity, url, false);
}
}
if (mTabList.size() == 0) {
newTab(activity, null, false);
}
finishInitialization();
subscriber.onComplete();
}
});
}
/**
* Method used to resume all the tabs in the browser.
* This is necessary because we cannot pause the
* WebView when the app is open currently due to a
* bug in the WebView, where calling onResume doesn't
* consistently resume it.
*
* @param context the context needed to initialize
* the LightningView preferences.
*/
public void resumeAll(@NonNull Context context) {
LightningView current = getCurrentTab();
if (current != null) {
current.resumeTimers();
}
for (LightningView tab : mTabList) {
if (tab != null) {
tab.onResume();
tab.initializePreferences(context);
}
}
}
/**
* Method used to pause all the tabs in the browser.
* This is necessary because we cannot pause the
* WebView when the app is open currently due to a
* bug in the WebView, where calling onResume doesn't
* consistently resume it.
*/
public void pauseAll() {
LightningView current = getCurrentTab();
if (current != null) {
current.pauseTimers();
}
for (LightningView tab : mTabList) {
if (tab != null) {
tab.onPause();
}
}
}
/**
* Return the tab at the given position in tabs list, or
* null if position is not in tabs list range.
*
* @param position the index in tabs list
* @return the corespondent {@link LightningView},
* or null if the index is invalid
*/
@Nullable
public synchronized LightningView getTabAtPosition(final int position) {
if (position < 0 || position >= mTabList.size()) {
return null;
}
return mTabList.get(position);
}
/**
* Frees memory for each tab in the
* manager. Note: this will only work
* on API < KITKAT as on KITKAT onward
* the WebViews manage their own
* memory correctly.
*/
public synchronized void freeMemory() {
for (LightningView tab : mTabList) {
//noinspection deprecation
tab.freeMemory();
}
}
/**
* Shutdown the manager. This destroys
* all tabs and clears the references
* to those tabs. Current tab is also
* released for garbage collection.
*/
public synchronized void shutdown() {
for (LightningView tab : mTabList) {
tab.onDestroy();
}
mTabList.clear();
mIsInitialized = false;
mCurrentTab = null;
}
/**
* Forwards network connection status to the WebViews.
*
* @param isConnected whether there is a network
* connection or not.
*/
public synchronized void notifyConnectionStatus(final boolean isConnected) {
for (LightningView tab : mTabList) {
final WebView webView = tab.getWebView();
if (webView != null) {
webView.setNetworkAvailable(isConnected);
}
}
}
/**
* The current number of tabs in the manager.
*
* @return the number of tabs in the list.
*/
public synchronized int size() {
return mTabList.size();
}
/**
* The index of the last tab in the manager.
*
* @return the last tab in the list or -1 if there are no tabs.
*/
public synchronized int last() {
return mTabList.size() - 1;
}
/**
* The last tab in the tab manager.
*
* @return the last tab, or null if
* there are no tabs.
*/
@Nullable
public synchronized LightningView lastTab() {
if (last() < 0) {
return null;
}
return mTabList.get(last());
}
/**
* Create and return a new tab. The tab is
* automatically added to the tabs list.
*
* @param activity the activity needed to create the tab.
* @param url the URL to initialize the tab with.
* @param isIncognito whether the tab is an incognito
* tab or not.
* @return a valid initialized tab.
*/
@NonNull
public synchronized LightningView newTab(@NonNull final Activity activity,
@Nullable final String url,
final boolean isIncognito) {
Log.d(TAG, "New tab");
final LightningView tab = new LightningView(activity, url, isIncognito);
mTabList.add(tab);
if (mTabNumberListener != null) {
mTabNumberListener.tabNumberChanged(size());
}
return tab;
}
/**
* Removes a tab from the list and destroys the tab.
* If the tab removed is the current tab, the reference
* to the current tab will be nullified.
*
* @param position The position of the tab to remove.
*/
private synchronized void removeTab(final int position) {
if (position >= mTabList.size()) {
return;
}
final LightningView tab = mTabList.remove(position);
if (mCurrentTab == tab) {
mCurrentTab = null;
}
tab.onDestroy();
}
/**
* Deletes a tab from the manager. If the tab
* being deleted is the current tab, this method
* will switch the current tab to a new valid tab.
*
* @param position the position of the tab to delete.
* @return returns true if the current tab
* was deleted, false otherwise.
*/
public synchronized boolean deleteTab(int position) {
Log.d(TAG, "Delete tab: " + position);
final LightningView currentTab = getCurrentTab();
int current = positionOf(currentTab);
if (current == position) {
if (size() == 1) {
mCurrentTab = null;
} else if (current < size() - 1) {
// There is another tab after this one
switchToTab(current + 1);
} else {
switchToTab(current - 1);
}
}
removeTab(position);
if (mTabNumberListener != null) {
mTabNumberListener.tabNumberChanged(size());
}
return current == position;
}
/**
* Return the position of the given tab.
*
* @param tab the tab to look for.
* @return the position of the tab or -1
* if the tab is not in the list.
*/
public synchronized int positionOf(final LightningView tab) {
return mTabList.indexOf(tab);
}
/**
* Saves the state of the current WebViews,
* to a bundle which is then stored in persistent
* storage and can be unparceled.
*/
public void saveState() {
Bundle outState = new Bundle(ClassLoader.getSystemClassLoader());
Log.d(Constants.TAG, "Saving tab state");
for (int n = 0; n < mTabList.size(); n++) {
LightningView tab = mTabList.get(n);
Bundle state = new Bundle(ClassLoader.getSystemClassLoader());
if (tab.getWebView() != null && !UrlUtils.isSpecialUrl(tab.getUrl())) {
tab.getWebView().saveState(state);
outState.putBundle(BUNDLE_KEY + n, state);
} else if (tab.getWebView() != null) {
state.putString(URL_KEY, tab.getUrl());
outState.putBundle(BUNDLE_KEY + n, state);
}
}
FileUtils.writeBundleToStorage(mApp, outState, BUNDLE_STORAGE);
}
/**
* Use this method to clear the saved
* state if you do not wish it to be
* restored when the browser next starts.
*/
public void clearSavedState() {
FileUtils.deleteBundleInStorage(mApp, BUNDLE_STORAGE);
}
/**
* Restores the previously saved tabs from the
* bundle stored in peristent file storage.
* It will create new tabs for each tab saved
* and will delete the saved instance file when
* restoration is complete.
*/
private Observable<Bundle> restoreState() {
return Observable.create(new Action<Bundle>() {
@Override
public void onSubscribe(@NonNull Subscriber<Bundle> subscriber) {
Bundle savedState = FileUtils.readBundleFromStorage(mApp, BUNDLE_STORAGE);
if (savedState != null) {
Log.d(Constants.TAG, "Restoring previous WebView state now");
for (String key : savedState.keySet()) {
if (key.startsWith(BUNDLE_KEY)) {
subscriber.onNext(savedState.getBundle(key));
}
}
}
FileUtils.deleteBundleInStorage(mApp, BUNDLE_STORAGE);
subscriber.onComplete();
}
});
}
/**
* 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.
*
* @return Return the index of the current tab, or -1 if the
* current tab is null.
*/
public int indexOfCurrentTab() {
return mTabList.indexOf(mCurrentTab);
}
/**
* Returns the index of the tab.
*
* @return Return the index of the tab, or -1 if the tab isn't in the list.
*/
public int indexOfTab(LightningView tab) {
return mTabList.indexOf(tab);
}
/**
* Return the current {@link LightningView} or null if
* no current tab has been set.
*
* @return a {@link LightningView} or null if there
* is no current tab.
*/
@Nullable
public synchronized LightningView getCurrentTab() {
return mCurrentTab;
}
/**
* Switch the current tab to the one at the given position.
* It returns the selected tab that has been switced to.
*
* @return the selected tab or null if position is out of tabs range.
*/
@Nullable
public synchronized LightningView switchToTab(final int position) {
Log.d(TAG, "switch to tab: " + position);
if (position < 0 || position >= mTabList.size()) {
Log.e(TAG, "Returning a null LightningView requested for position: " + position);
return null;
} else {
final LightningView tab = mTabList.get(position);
if (tab != null) {
mCurrentTab = tab;
}
return tab;
}
}
}
@@ -3,20 +3,31 @@ package acr.browser.lightning.activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import java.util.ArrayDeque;
import java.util.Queue;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.preference.PreferenceManager;
public abstract class ThemableBrowserActivity extends AppCompatActivity {
@Inject PreferenceManager mPreferences;
private int mTheme;
private boolean mShowTabsInDrawer;
private boolean mShouldRunOnResumeActions = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
mTheme = PreferenceManager.getInstance().getUseTheme();
mShowTabsInDrawer = PreferenceManager.getInstance().getShowTabsInDrawer(!isTablet());
BrowserApp.getAppComponent().inject(this);
mTheme = mPreferences.getUseTheme();
mShowTabsInDrawer = mPreferences.getShowTabsInDrawer(!isTablet());
// set the theme
if (mTheme == 1) {
@@ -27,23 +38,42 @@ public abstract class ThemableBrowserActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus && mShouldRunOnResumeActions) {
mShouldRunOnResumeActions = false;
onWindowVisibleToUserAfterResume();
}
}
/**
* Called after the activity is resumed
* and the UI becomes visible to the user.
* Called by onWindowFocusChanged only if
* onResume has been called.
*/
public void onWindowVisibleToUserAfterResume() {
}
@Override
protected void onResume() {
super.onResume();
int theme = PreferenceManager.getInstance().getUseTheme();
boolean drawerTabs = PreferenceManager.getInstance().getShowTabsInDrawer(!isTablet());
mShouldRunOnResumeActions = true;
int theme = mPreferences.getUseTheme();
boolean drawerTabs = mPreferences.getShowTabsInDrawer(!isTablet());
if (theme != mTheme || mShowTabsInDrawer != drawerTabs) {
restart();
}
}
public boolean isTablet() {
boolean isTablet() {
return (getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE;
}
private void restart() {
Intent intent = getIntent();
finish();
startActivity(intent);
startActivity(new Intent(this, getClass()));
}
}
@@ -3,7 +3,10 @@ package acr.browser.lightning.activity;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.ThemeUtils;
@@ -11,9 +14,12 @@ public abstract class ThemableSettingsActivity extends AppCompatPreferenceActivi
private int mTheme;
@Inject PreferenceManager mPreferenceManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
mTheme = PreferenceManager.getInstance().getUseTheme();
BrowserApp.getAppComponent().inject(this);
mTheme = mPreferenceManager.getUseTheme();
// set the theme
if (mTheme == 0) {
@@ -32,7 +38,7 @@ public abstract class ThemableSettingsActivity extends AppCompatPreferenceActivi
@Override
protected void onResume() {
super.onResume();
if (PreferenceManager.getInstance().getUseTheme() != mTheme) {
if (mPreferenceManager.getUseTheme() != mTheme) {
restart();
}
}
@@ -0,0 +1,73 @@
package acr.browser.lightning.app;
import javax.inject.Singleton;
import acr.browser.lightning.activity.BrowserActivity;
import acr.browser.lightning.activity.ReadingActivity;
import acr.browser.lightning.activity.TabsManager;
import acr.browser.lightning.activity.ThemableBrowserActivity;
import acr.browser.lightning.activity.ThemableSettingsActivity;
import acr.browser.lightning.browser.BrowserPresenter;
import acr.browser.lightning.constant.StartPage;
import acr.browser.lightning.dialog.LightningDialogBuilder;
import acr.browser.lightning.download.LightningDownloadListener;
import acr.browser.lightning.fragment.BookmarkSettingsFragment;
import acr.browser.lightning.fragment.BookmarksFragment;
import acr.browser.lightning.fragment.DebugSettingsFragment;
import acr.browser.lightning.fragment.LightningPreferenceFragment;
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.LightningView;
import acr.browser.lightning.view.LightningWebClient;
import dagger.Component;
@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
void inject(BrowserActivity activity);
void inject(BookmarksFragment fragment);
void inject(BookmarkSettingsFragment fragment);
void inject(SuggestionsAdapter adapter);
void inject(LightningDialogBuilder builder);
void inject(TabsFragment fragment);
void inject(LightningView lightningView);
void inject(ThemableBrowserActivity activity);
void inject(LightningPreferenceFragment fragment);
void inject(BrowserApp app);
void inject(ProxyUtils proxyUtils);
void inject(ReadingActivity activity);
void inject(LightningWebClient webClient);
void inject(ThemableSettingsActivity activity);
void inject(AdBlock adBlock);
void inject(LightningDownloadListener listener);
void inject(PrivacySettingsFragment fragment);
void inject(StartPage startPage);
void inject(BrowserPresenter presenter);
void inject(TabsManager manager);
void inject(DebugSettingsFragment fragment);
}
@@ -0,0 +1,49 @@
package acr.browser.lightning.app;
import android.app.Application;
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 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
public Application provideApplication() {
return mApp;
}
@Provides
public Context provideContext() {
return mApp.getApplicationContext();
}
@NonNull
@Provides
public Bus provideBus() {
return mBus;
}
@NonNull
@Provides
@Singleton
public I2PAndroidHelper provideI2PAndroidHelper() {
return new I2PAndroidHelper(mApp.getApplicationContext());
}
}
@@ -0,0 +1,88 @@
package acr.browser.lightning.app;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.util.Log;
import android.webkit.WebView;
import com.squareup.leakcanary.LeakCanary;
import com.squareup.otto.Bus;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javax.inject.Inject;
import acr.browser.lightning.BuildConfig;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.MemoryLeakUtils;
public class BrowserApp extends Application {
private static final String TAG = BrowserApp.class.getSimpleName();
private static AppComponent mAppComponent;
private static final Executor mIOThread = Executors.newSingleThreadExecutor();
private static final Executor mTaskThread = Executors.newCachedThreadPool();
@Inject Bus mBus;
@Inject PreferenceManager mPreferenceManager;
@Override
public void onCreate() {
super.onCreate();
mAppComponent = DaggerAppComponent.builder().appModule(new AppModule(this)).build();
mAppComponent.inject(this);
if (mPreferenceManager.getUseLeakCanary() && !isRelease()) {
LeakCanary.install(this);
}
if (!isRelease() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
registerActivityLifecycleCallbacks(new MemoryLeakUtils.LifecycleAdapter() {
@Override
public void onActivityDestroyed(Activity activity) {
Log.d(TAG, "Cleaning up after the Android framework");
MemoryLeakUtils.clearNextServedView(BrowserApp.this);
}
});
}
@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;
}
/**
* Determines whether this is a release build.
*
* @return true if this is a release build, false otherwise.
*/
public static boolean isRelease() {
return !BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.toLowerCase().equals("release");
}
}
@@ -0,0 +1,53 @@
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<Runnable> 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");
}
}
}
@@ -0,0 +1,162 @@
package acr.browser.lightning.async;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.widget.ImageView;
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<Void, Void, Bitmap> {
private static final String TAG = ImageDownloadTask.class.getSimpleName();
@NonNull private final WeakReference<ImageView> mFaviconImage;
@NonNull private final WeakReference<Context> mContextReference;
@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 Context 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.mContextReference = new WeakReference<>(context.getApplicationContext());
}
@Nullable
@Override
protected Bitmap doInBackground(Void... params) {
Bitmap mIcon = null;
// unique path for each url that is bookmarked.
if (mUrl == null) {
return mDefaultBitmap;
}
Context context = mContextReference.get();
if (context == null) {
return mDefaultBitmap;
}
File cache = context.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";
// 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())) {
Context context = view.getContext();
if (context instanceof Activity) {
((Activity) context).runOnUiThread(new Runnable() {
@Override
public void run() {
view.setImageBitmap(fav);
}
});
} else {
view.setImageBitmap(fav);
}
}
mWeb.setBitmap(fav);
}
}
@@ -0,0 +1,334 @@
package acr.browser.lightning.browser;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.squareup.otto.Bus;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.activity.TabsManager;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.controller.UIController;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.react.OnSubscribe;
import acr.browser.lightning.utils.UrlUtils;
import acr.browser.lightning.view.LightningView;
/**
* Presenter in charge of keeping track of
* the current tab and setting the current tab
* of the
*/
public class BrowserPresenter {
private static final String TAG = BrowserPresenter.class.getSimpleName();
@NonNull private final TabsManager mTabsModel;
@Inject PreferenceManager mPreferences;
@Inject Bus mEventBus;
@NonNull private final BrowserView mView;
@Nullable private LightningView mCurrentTab;
private final boolean mIsIncognito;
private boolean mShouldClose;
public BrowserPresenter(@NonNull BrowserView view, boolean isIncognito) {
BrowserApp.getAppComponent().inject(this);
mTabsModel = ((UIController) view).getTabModel();
mView = view;
mIsIncognito = isIncognito;
mTabsModel.setTabNumberChangedListener(new TabsManager.TabNumberChangedListener() {
@Override
public void tabNumberChanged(int newNumber) {
mView.updateTabNumber(newNumber);
}
});
}
/**
* Initializes the tab manager with the new intent
* that is handed in by the BrowserActivity.
*
* @param intent the intent to handle, may be null.
*/
public void setupTabs(@Nullable Intent intent) {
mTabsModel.initializeTabs((Activity) mView, intent, mIsIncognito)
.subscribe(new OnSubscribe<Void>() {
@Override
public void onComplete() {
// At this point we always have at least a tab in the tab manager
tabChanged(mTabsModel.last());
mView.updateTabNumber(mTabsModel.size());
}
});
}
/**
* Notify the presenter that a change occurred to
* the current tab. Currently doesn't do anything
* other than tell the view to notify the adapter
* about the change.
*
* @param tab the tab that changed, may be null.
*/
public void tabChangeOccurred(@Nullable LightningView tab) {
mView.notifyTabViewChanged(mTabsModel.indexOfTab(tab));
}
private void onTabChanged(@Nullable LightningView newTab) {
Log.d(TAG, "On tab changed");
if (newTab == null) {
mView.removeTabView();
if (mCurrentTab != null) {
mCurrentTab.pauseTimers();
mCurrentTab.onDestroy();
}
} else {
if (newTab.getWebView() == null) {
mView.removeTabView();
if (mCurrentTab != null) {
mCurrentTab.pauseTimers();
mCurrentTab.onDestroy();
}
} else {
if (mCurrentTab != null) {
// TODO: Restore this when Google fixes the bug where the WebView is
// blank after calling onPause followed by onResume.
// mCurrentTab.onPause();
mCurrentTab.setForegroundTab(false);
}
newTab.resumeTimers();
newTab.onResume();
newTab.setForegroundTab(true);
mView.updateProgress(newTab.getProgress());
mView.setBackButtonEnabled(newTab.canGoBack());
mView.setForwardButtonEnabled(newTab.canGoForward());
mView.updateUrl(newTab.getUrl(), true);
mView.setTabView(newTab.getWebView());
int index = mTabsModel.indexOfTab(newTab);
if (index >= 0) {
mView.notifyTabViewChanged(mTabsModel.indexOfTab(newTab));
}
}
}
mCurrentTab = newTab;
}
/**
* Closes all tabs but the current tab.
*/
public void closeAllOtherTabs() {
while (mTabsModel.last() != mTabsModel.indexOfCurrentTab()) {
deleteTab(mTabsModel.last());
}
while (0 != mTabsModel.indexOfCurrentTab()) {
deleteTab(0);
}
}
/**
* Deletes the tab at the specified position.
*
* @param position the position at which to
* delete the tab.
*/
public void deleteTab(int position) {
Log.d(TAG, "delete Tab");
final LightningView tabToDelete = mTabsModel.getTabAtPosition(position);
if (tabToDelete == null) {
return;
}
if (!UrlUtils.isSpecialUrl(tabToDelete.getUrl()) && !mIsIncognito) {
mPreferences.setSavedUrl(tabToDelete.getUrl());
}
final boolean isShown = tabToDelete.isShown();
boolean shouldClose = mShouldClose && isShown && Boolean.TRUE.equals(tabToDelete.getTag());
final LightningView currentTab = mTabsModel.getCurrentTab();
if (mTabsModel.size() == 1 && currentTab != null &&
(UrlUtils.isSpecialUrl(currentTab.getUrl()) ||
currentTab.getUrl().equals(mPreferences.getHomepage()))) {
mView.closeActivity();
return;
} else {
if (isShown) {
mView.removeTabView();
}
boolean currentDeleted = mTabsModel.deleteTab(position);
if (currentDeleted) {
tabChanged(mTabsModel.indexOfCurrentTab());
}
}
final LightningView afterTab = mTabsModel.getCurrentTab();
mView.notifyTabViewRemoved(position);
if (afterTab == null) {
mView.closeBrowser();
return;
} else if (afterTab != currentTab) {
//TODO remove this?
// switchTabs(currentTab, afterTab);
// if (currentTab != null) {
// currentTab.pauseTimers();
// }
mView.notifyTabViewChanged(mTabsModel.indexOfCurrentTab());
}
if (shouldClose) {
mShouldClose = false;
mView.closeActivity();
}
mView.updateTabNumber(mTabsModel.size());
Log.d(TAG, "deleted tab");
}
/**
* Handle a new intent from the the main
* BrowserActivity.
*
* @param intent the intent to handle,
* may be null.
*/
public void onNewIntent(@Nullable final Intent intent) {
mTabsModel.doAfterInitialization(new Runnable() {
@Override
public void run() {
final String url;
if (intent != null) {
url = intent.getDataString();
} else {
url = null;
}
int num = 0;
if (intent != null && intent.getExtras() != null) {
num = intent.getExtras().getInt(Constants.INTENT_ORIGIN);
}
if (num == 1) {
loadUrlInCurrentView(url);
} else if (url != null) {
if (url.startsWith(Constants.FILE)) {
mView.showBlockedLocalFileDialog(new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
newTab(url, true);
}
});
} else {
newTab(url, true);
}
mShouldClose = true;
LightningView tab = mTabsModel.lastTab();
if (tab != null) {
tab.setTag(true);
}
}
}
});
}
/**
* Loads a URL in the current tab.
*
* @param url the URL to load, must
* not be null.
*/
public void loadUrlInCurrentView(@NonNull final String url) {
final LightningView currentTab = mTabsModel.getCurrentTab();
if (currentTab == null) {
// This is a problem, probably an assert will be better than a return
return;
}
currentTab.loadUrl(url);
}
/**
* Notifies the presenter that it should
* shut down. This should be called when
* the BrowserActivity is destroyed so that
* we don't leak any memory.
*/
public void shutdown() {
onTabChanged(null);
mTabsModel.setTabNumberChangedListener(null);
mTabsModel.cancelPendingWork();
}
/**
* Notifies the presenter that we wish
* to switch to a different tab at the
* specified position. If the position
* is not in the model, this method will
* do nothing.
*
* @param position the position of the
* tab to switch to.
*/
public synchronized void tabChanged(int position) {
Log.d(TAG, "tabChanged: " + position);
if (position < 0 || position >= mTabsModel.size()) {
return;
}
LightningView tab = mTabsModel.switchToTab(position);
onTabChanged(tab);
}
/**
* Open a new tab with the specified URL. You
* can choose to show the tab or load it in the
* background.
*
* @param url the URL to load, may be null if you
* don't wish to load anything.
* @param show whether or not to switch to this
* tab after opening it.
* @return true if we successfully created the tab,
* false if we have hit max tabs.
*/
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) {
mView.showSnackbar(R.string.max_tabs);
return false;
}
Log.d(TAG, "New tab, show: " + show);
LightningView startingTab = mTabsModel.newTab((Activity) mView, url, mIsIncognito);
if (mTabsModel.size() == 1) {
startingTab.resumeTimers();
}
mView.notifyTabViewAdded();
if (show) {
LightningView tab = mTabsModel.switchToTab(mTabsModel.last());
onTabChanged(tab);
}
mView.updateTabNumber(mTabsModel.size());
return true;
}
}
@@ -0,0 +1,38 @@
package acr.browser.lightning.browser;
import android.content.DialogInterface;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.view.View;
public interface BrowserView {
void setTabView(@NonNull View view);
void removeTabView();
void updateUrl(String url, boolean shortUrl);
void updateProgress(int progress);
void updateTabNumber(int number);
void closeBrowser();
void closeActivity();
void showBlockedLocalFileDialog(DialogInterface.OnClickListener listener);
void showSnackbar(@StringRes int resource);
void setForwardButtonEnabled(boolean enabled);
void setBackButtonEnabled(boolean enabled);
void notifyTabViewRemoved(int position);
void notifyTabViewAdded();
void notifyTabViewChanged(int position);
}
@@ -0,0 +1,11 @@
package acr.browser.lightning.browser;
public interface TabsView {
void tabAdded();
void tabRemoved(int position);
void tabChanged(int position);
}
@@ -0,0 +1,49 @@
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;
}
}
/**
* The user ask to add/del a bookmark to the currently displayed page
*/
public static class ToggleBookmarkForCurrentPage {
}
/**
* Sended by the {@link acr.browser.lightning.fragment.BookmarksFragment} when it wants to close
* itself (generally in reply to a {@link acr.browser.lightning.bus.BrowserEvents.UserPressedBack}
* event.
*/
public static class CloseBookmarks {
}
/**
* 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;
}
}
}
@@ -0,0 +1,90 @@
package acr.browser.lightning.bus;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
public final class BrowserEvents {
private BrowserEvents() {
// No instances
}
/**
* The {@link acr.browser.lightning.activity.BrowserActivity} signal a new bookmark was added
* (mainly to the {@link acr.browser.lightning.fragment.BookmarksFragment}).
*/
public static class BookmarkAdded {
public final String title, url;
public BookmarkAdded(final String title, final String url) {
this.title = title;
this.url = url;
}
}
/**
* Notify the current page has a new url. This is generally used to update the
* {@link acr.browser.lightning.fragment.BookmarksFragment} interface.
*/
public static class CurrentPageUrl {
public final String url;
public CurrentPageUrl(final String url) {
this.url = url;
}
}
/**
* Notify the BookmarksFragment and TabsFragment that the user pressed the back button
*/
public static class UserPressedBack {
}
/**
*
*/
/**
* Notify the Browser to display a SnackBar in the main activity
*/
public static class ShowSnackBarMessage {
@Nullable public final String message;
@StringRes
public final int stringRes;
public ShowSnackBarMessage(@Nullable final String message) {
this.message = message;
this.stringRes = -1;
}
public ShowSnackBarMessage(@StringRes final int stringRes) {
this.message = null;
this.stringRes = stringRes;
}
}
public final static class OpenHistoryInCurrentTab {
}
/**
* The user want to open the given url in the current tab
*/
public final static class OpenUrlInCurrentTab {
public final String url;
public OpenUrlInCurrentTab(final String url) {
this.url = url;
}
}
/**
* The user ask to open the given url as new tab
*/
public final static class OpenUrlInNewTab {
public final String url;
public OpenUrlInNewTab(final String url) {
this.url = url;
}
}
}
@@ -0,0 +1,34 @@
package acr.browser.lightning.bus;
/**
* Collections of navigation events, like go back or go forward
*
* @author Stefano Pacifici
* @date 2015/09/15
*/
public class NavigationEvents {
private NavigationEvents() {
// No instances please
}
/**
* Fired by {@link acr.browser.lightning.fragment.TabsFragment} when the user presses back
* button.
*/
public static class GoBack {
}
/**
* Fired by {@link acr.browser.lightning.fragment.TabsFragment} when the user presses forward
* button.
*/
public static class GoForward {
}
/**
* Fired by {@link acr.browser.lightning.fragment.TabsFragment} when the user presses the home
* button.
*/
public static class GoHome {
}
}
@@ -0,0 +1,65 @@
package acr.browser.lightning.bus;
/**
* A collection of events been sent by {@link acr.browser.lightning.fragment.TabsFragment}
*
* @author Stefano Pacifici
* @date 2015/09/14
*/
public final class TabEvents {
private TabEvents() {
// No instances
}
/**
* Sended by {@link acr.browser.lightning.fragment.TabsFragment} when the user click on the
* tab exit button
*/
public static class CloseTab {
public final int position;
public CloseTab(int position) {
this.position = position;
}
}
/**
* Sended by {@link acr.browser.lightning.fragment.TabsFragment} when the user click on the
* tab itself.
*/
public static class ShowTab {
public final int position;
public ShowTab(int position) {
this.position = position;
}
}
/**
* Sended by {@link acr.browser.lightning.fragment.TabsFragment} when the user long press on the
* tab itself.
*/
public static class ShowCloseDialog {
public final int position;
public ShowCloseDialog(int position) {
this.position = position;
}
}
/**
* Sended by {@link acr.browser.lightning.fragment.TabsFragment} when the user want to create a
* new tab.
*/
public static class NewTab {
}
/**
* Sended by {@link acr.browser.lightning.fragment.TabsFragment} when the user long presses on
* new tab button.
*/
public static class NewTabLongPress {
}
}
@@ -4,88 +4,148 @@
package acr.browser.lightning.constant;
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 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 acr.browser.lightning.R;
import acr.browser.lightning.activity.BrowserApp;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.database.BookmarkManager;
import acr.browser.lightning.database.HistoryItem;
import acr.browser.lightning.utils.ThemeUtils;
import acr.browser.lightning.utils.Utils;
import acr.browser.lightning.view.LightningView;
public class BookmarkPage {
public final class BookmarkPage extends AsyncTask<Void, Void, Void> {
/**
* The bookmark page standard suffix
*/
public static final String FILENAME = "bookmarks.html";
public static final String HEADING = "<!DOCTYPE html><html xmlns=http://www.w3.org/1999/xhtml>\n" +
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'>\n" +
"<title>" +
BrowserApp.getAppContext().getString(R.string.action_bookmarks) +
"</title>\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 = "</title>\n" +
"</head>\n" +
"<style>body{background:#e1e1e1;max-width:100%;min-height:100%}#content{width:100%;max-width:800px;margin:0 auto;text-align:center}.box{vertical-align:middle;text-align:center;position:relative;display:inline-block;height:45px;width:150px;margin:10px;background-color:#fff;box-shadow:0 3px 6px rgba(0,0,0,0.25);font-family:Arial;color:#444;font-size:12px;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px}.box-content{height:25px;width:100%;vertical-align:middle;text-align:center;display:table-cell}p.ellipses{" +
"width:130px;font-size: small;font-family: Arial, Helvetica, 'sans-serif';white-space:nowrap;overflow:hidden;text-align:left;vertical-align:middle;margin:auto;text-overflow:ellipsis;-o-text-overflow:ellipsis;-ms-text-overflow:ellipsis}.box a{width:100%;height:100%;position:absolute;left:0;top:0}img{vertical-align:middle;margin-right:10px;width:20px;height:20px;}.margin{margin:10px}</style>\n" +
"<body><div id=content>";
public static final String PART1 = "<div class=box><a href='";
private static final String PART1 = "<div class=box><a href='";
public static final String PART2 = "'></a>\n" +
private static final String PART2 = "'></a>\n" +
"<div class=margin>\n" +
"<div class=box-content>\n" +
"<p class=ellipses>\n" +
"<img src='";
public static final String PART3 = "http://www.google.com/s2/favicons?domain=";
private static final String PART3 = "https://www.google.com/s2/favicons?domain=";
public static final String PART4 = "' />";
private static final String PART4 = "' />";
public static final String PART5 = "</p></div></div></div>";
private static final String PART5 = "</p></div></div></div>";
public static final String END = "</div></body></html>";
private static final String END = "</div></body></html>";
public static void buildBookmarkPage(final Activity activity, final String folder, final List<HistoryItem> list) {
final BookmarkManager manager = BookmarkManager.getInstance(activity);
File bookmarkWebPage;
if (folder == null || folder.isEmpty()) {
bookmarkWebPage = new File(activity.getFilesDir(), BookmarkPage.FILENAME);
} else {
bookmarkWebPage = new File(activity.getFilesDir(), folder + '-' + BookmarkPage.FILENAME);
private File mFilesDir;
private File mCacheDir;
private final Application mApp;
private final BookmarkManager mManager;
@NonNull private final WeakReference<LightningView> mTabReference;
private final Bitmap mFolderIcon;
@NonNull private final String mTitle;
public BookmarkPage(LightningView tab, @NonNull Activity activity, BookmarkManager manager) {
mApp = BrowserApp.get(activity);
final Bitmap folderIcon = ThemeUtils.getThemedBitmap(activity, R.drawable.ic_folder, false);
mTitle = mApp.getString(R.string.action_bookmarks);
mManager = manager;
mTabReference = new WeakReference<>(tab);
mFolderIcon = folderIcon;
}
@Override
protected Void doInBackground(Void... params) {
mCacheDir = mApp.getCacheDir();
mFilesDir = mApp.getFilesDir();
cacheDefaultFolderIcon();
buildBookmarkPage(null, mManager);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
LightningView tab = mTabReference.get();
if (tab != null) {
File bookmarkWebPage = new File(mFilesDir, FILENAME);
tab.loadUrl(Constants.FILE + bookmarkWebPage);
}
final StringBuilder bookmarkBuilder = new StringBuilder(BookmarkPage.HEADING);
}
String folderIconPath = Constants.FILE + activity.getCacheDir() + "/folder.png";
for (int n = 0; n < list.size(); n++) {
private void cacheDefaultFolderIcon() {
FileOutputStream outputStream = null;
File image = new File(mCacheDir, "folder.png");
try {
outputStream = new FileOutputStream(image);
mFolderIcon.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
mFolderIcon.recycle();
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
Utils.close(outputStream);
}
}
private void buildBookmarkPage(@Nullable final String folder, @NonNull final BookmarkManager manager) {
final List<HistoryItem> list = manager.getBookmarksCopyFromFolder(folder, true);
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.png";
for (int n = 0, size = list.size(); n < size; n++) {
final HistoryItem item = list.get(n);
bookmarkBuilder.append(BookmarkPage.PART1);
bookmarkBuilder.append(PART1);
if (item.isFolder()) {
File folderPage = new File(activity.getFilesDir(), item.getTitle() + '-' + BookmarkPage.FILENAME);
final File folderPage = new File(mFilesDir, item.getTitle() + '-' + FILENAME);
bookmarkBuilder.append(Constants.FILE).append(folderPage);
bookmarkBuilder.append(BookmarkPage.PART2);
bookmarkBuilder.append(PART2);
bookmarkBuilder.append(folderIconPath);
new Thread(new Runnable() {
@Override
public void run() {
buildBookmarkPage(activity, item.getTitle(), manager.getBookmarksFromFolder(item.getTitle(), true));
}
}).run();
buildBookmarkPage(item.getTitle(), manager);
} else {
bookmarkBuilder.append(item.getUrl());
bookmarkBuilder.append(BookmarkPage.PART2).append(BookmarkPage.PART3);
bookmarkBuilder.append(PART2).append(PART3);
bookmarkBuilder.append(item.getUrl());
}
bookmarkBuilder.append(BookmarkPage.PART4);
bookmarkBuilder.append(PART4);
bookmarkBuilder.append(item.getTitle());
bookmarkBuilder.append(BookmarkPage.PART5);
bookmarkBuilder.append(PART5);
}
bookmarkBuilder.append(BookmarkPage.END);
bookmarkBuilder.append(END);
FileWriter bookWriter = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
bookWriter = new FileWriter(bookmarkWebPage, false);
bookWriter.write(bookmarkBuilder.toString());
} catch (IOException e) {
@@ -95,4 +155,8 @@ public class BookmarkPage {
}
}
public void load() {
executeOnExecutor(BrowserApp.getIOThread());
}
}
@@ -3,8 +3,6 @@
*/
package acr.browser.lightning.constant;
import android.os.Environment;
import acr.browser.lightning.BuildConfig;
public final class Constants {
@@ -27,10 +25,24 @@ public final class Constants {
public static final String HOMEPAGE = "about:home";
public static final String BAIDU_SEARCH = "https://www.baidu.com/s?wd=";
public static final String YANDEX_SEARCH = "https://yandex.ru/yandsearch?lr=21411&text=";
public static final String EXTERNAL_STORAGE = Environment.getExternalStorageDirectory()
.toString();
public static final String JAVASCRIPT_INVERT_PAGE = "javascript:(function(){var e='img {-webkit-filter: invert(100%);'+'-moz-filter: invert(100%);'+'-o-filter: invert(100%);'+'-ms-filter: invert(100%); }',t=document.getElementsByTagName('head')[0],n=document.createElement('style');if(!window.counter){window.counter=1}else{window.counter++;if(window.counter%2==0){var e='html {-webkit-filter: invert(0%); -moz-filter: invert(0%); -o-filter: invert(0%); -ms-filter: invert(0%); }'}}n.type='text/css';if(n.styleSheet){n.styleSheet.cssText=e}else{n.appendChild(document.createTextNode(e))}t.appendChild(n)})();";
public static final String JAVASCRIPT_TEXT_REFLOW = "javascript:document.getElementsByTagName('body')[0].style.width=window.innerWidth+'px';";
public static final String JAVASCRIPT_THEME_COLOR = "(function () {\n" +
" \"use strict\";\n" +
" var metas, i, tag;\n" +
" metas = document.getElementsByTagName('meta');\n" +
" if (metas !== null) {\n" +
" for (i = 0; i < metas.length; i++) {\n" +
" tag = metas[i].getAttribute('name');\n" +
" if (tag !== null && tag.toLowerCase() === 'theme-color') {\n" +
" return metas[i].getAttribute('content');\n" +
" }\n" +
" console.log(tag);\n" +
" }\n" +
" }\n" +
'\n' +
" return '';\n" +
"}());";
public static final String LOAD_READING_URL = "ReadingUrl";
@@ -50,4 +62,6 @@ public final class Constants {
public static final String DEFAULT_ENCODING = "UTF-8";
public static final String[] TEXT_ENCODINGS = {"ISO-8859-1", "UTF-8", "GBK", "Big5", "ISO-2022-JP", "SHIFT_JS", "EUC-JP", "EUC-KR"};
public static final String INTENT_ORIGIN = "URL_INTENT_ORIGIN";
}
@@ -3,27 +3,32 @@
*/
package acr.browser.lightning.constant;
import android.app.Application;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
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 android.content.Context;
import acr.browser.lightning.activity.BrowserApp;
import acr.browser.lightning.database.HistoryItem;
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.utils.Utils;
import acr.browser.lightning.view.LightningView;
public class HistoryPage {
public class HistoryPage extends AsyncTask<Void, Void, Void> {
public static final String FILENAME = "history.html";
private static final String HEADING = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\"><head><meta content=\"en-us\" http-equiv=\"Content-Language\" /><meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\"><title>"
+ BrowserApp.getAppContext().getString(R.string.action_history)
+ "</title></head><style>body { background: #e1e1e1;}.box { vertical-align:middle;position:relative; display: block; margin: 10px;padding-left:10px;padding-right:10px;padding-top:5px;padding-bottom:5px; background-color:#fff;box-shadow: 0px 2px 3px rgba( 0, 0, 0, 0.25 );font-family: Arial;color: #444;font-size: 12px;-moz-border-radius: 2px;-webkit-border-radius: 2px;border-radius: 2px;}.box a { width: 100%; height: 100%; position: absolute; left: 0; top: 0;}.black {color: black;font-size: 15px;font-family: Arial; white-space: nowrap; overflow: hidden;margin:auto; text-overflow: ellipsis; -o-text-overflow: ellipsis; -ms-text-overflow: ellipsis;}.font {color: gray;font-size: 10px;font-family: Arial; white-space: nowrap; overflow: hidden;margin:auto; text-overflow: ellipsis; -o-text-overflow: ellipsis; -ms-text-overflow: ellipsis;}</style><body><div id=\"content\">";
private static final String HEADING_1 = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\"><head><meta content=\"en-us\" http-equiv=\"Content-Language\" /><meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\"><title>";
private static final String HEADING_2 = "</title></head><style>body { background: #e1e1e1;}.box { vertical-align:middle;position:relative; display: block; margin: 10px;padding-left:10px;padding-right:10px;padding-top:5px;padding-bottom:5px; background-color:#fff;box-shadow: 0px 2px 3px rgba( 0, 0, 0, 0.25 );font-family: Arial;color: #444;font-size: 12px;-moz-border-radius: 2px;-webkit-border-radius: 2px;border-radius: 2px;}.box a { width: 100%; height: 100%; position: absolute; left: 0; top: 0;}.black {color: black;font-size: 15px;font-family: Arial; white-space: nowrap; overflow: hidden;margin:auto; text-overflow: ellipsis; -o-text-overflow: ellipsis; -ms-text-overflow: ellipsis;}.font {color: gray;font-size: 10px;font-family: Arial; white-space: nowrap; overflow: hidden;margin:auto; text-overflow: ellipsis; -o-text-overflow: ellipsis; -ms-text-overflow: ellipsis;}</style><body><div id=\"content\">";
private static final String PART1 = "<div class=\"box\"><a href=\"";
@@ -35,26 +40,58 @@ public class HistoryPage {
private static final String END = "</div></body></html>";
public static String getHistoryPage(Context context) {
StringBuilder historyBuilder = new StringBuilder(HistoryPage.HEADING);
List<HistoryItem> historyList = getWebHistory(context);
@NonNull private final WeakReference<LightningView> mTabReference;
@NonNull private final Application mApp;
@NonNull private final String mTitle;
private final HistoryDatabase mHistoryDatabase;
@Nullable private String mHistoryUrl = null;
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);
}
}
@NonNull
private String getHistoryPage() {
StringBuilder historyBuilder = new StringBuilder(HEADING_1 + mTitle + HEADING_2);
List<HistoryItem> historyList = mHistoryDatabase.getLastHundredItems();
Iterator<HistoryItem> it = historyList.iterator();
HistoryItem helper;
while (it.hasNext()) {
helper = it.next();
historyBuilder.append(HistoryPage.PART1);
historyBuilder.append(PART1);
historyBuilder.append(helper.getUrl());
historyBuilder.append(HistoryPage.PART2);
historyBuilder.append(PART2);
historyBuilder.append(helper.getTitle());
historyBuilder.append(HistoryPage.PART3);
historyBuilder.append(PART3);
historyBuilder.append(helper.getUrl());
historyBuilder.append(HistoryPage.PART4);
historyBuilder.append(PART4);
}
historyBuilder.append(HistoryPage.END);
File historyWebPage = new File(context.getFilesDir(), FILENAME);
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) {
@@ -65,9 +102,22 @@ public class HistoryPage {
return Constants.FILE + historyWebPage;
}
private static List<HistoryItem> getWebHistory(Context context) {
HistoryDatabase databaseHandler = HistoryDatabase.getInstance(context
.getApplicationContext());
return databaseHandler.getLastHundredItems();
public void load() {
executeOnExecutor(BrowserApp.getIOThread());
}
}
/**
* 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.
*
* @param application the application object needed to get the file.
*/
public static void deleteHistoryPage(@NonNull Application application) {
File historyWebPage = new File(application.getFilesDir(), FILENAME);
if (historyWebPage.exists()) {
historyWebPage.delete();
}
}
}
@@ -3,29 +3,36 @@
*/
package acr.browser.lightning.constant;
import android.app.Activity;
import android.app.Application;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.ref.WeakReference;
import javax.inject.Inject;
import acr.browser.lightning.activity.BrowserApp;
import acr.browser.lightning.R;
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 {
public class StartPage extends AsyncTask<Void, Void, Void> {
public static final String FILENAME = "homepage.html";
public static final String HEAD = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\">"
private static final String HEAD_1 = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\">"
+ "<head>"
+ "<meta content=\"en-us\" http-equiv=\"Content-Language\" />"
+ "<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\" />"
+ "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">"
+ "<title>"
+ BrowserApp.getAppContext().getString(R.string.home)
+ "</title>"
+ "<title>";
private static final String HEAD_2 = "</title>"
+ "</head>"
+ "<style>body{background:#f2f2f2;text-align:center;margin:0px;}#search_input{height:35px; "
+ "width:100%;outline:none;border:none;font-size: 16px;background-color:transparent;}"
@@ -41,11 +48,42 @@ public class StartPage {
+ "font-size: 12px;-moz-border-radius: 2px;-webkit-border-radius: 2px;"
+ "border-radius: 2px;}</style><body> <div class=\"outer\"><div class=\"middle\"><div class=\"inner\"><img class=\"smaller\" src=\"";
public static final String MIDDLE = "\" ></br></br><form onsubmit=\"return search()\" class=\"search_bar\">"
private static final String MIDDLE = "\" ></br></br><form onsubmit=\"return search()\" class=\"search_bar\" autocomplete=\"off\">"
+ "<input type=\"submit\" id=\"search_submit\" value=\"Search\" ><span><input class=\"search\" type=\"text\" value=\"\" id=\"search_input\" >"
+ "</span></form></br></br></div></div></div><script type=\"text/javascript\">function search(){if(document.getElementById(\"search_input\").value != \"\"){window.location.href = \"";
public static final String END = "\" + document.getElementById(\"search_input\").value;document.getElementById(\"search_input\").value = \"\";}return false;}</script></body></html>";
private static final String END = "\" + document.getElementById(\"search_input\").value;document.getElementById(\"search_input\").value = \"\";}return false;}</script></body></html>";
@NonNull private final String mTitle;
@NonNull private final Application mApp;
@NonNull private final WeakReference<LightningView> mTabReference;
@Inject PreferenceManager mPreferenceManager;
private String mStartpageUrl;
public StartPage(LightningView tab, @NonNull Application app) {
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;
}
@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
@@ -53,15 +91,16 @@ public class StartPage {
*
* @return the URL to load
*/
public static String getHomepage(Activity activity) {
StringBuilder homepageBuilder = new StringBuilder(StartPage.HEAD);
@NonNull
private String getHomepage() {
StringBuilder homepageBuilder = new StringBuilder(HEAD_1 + mTitle + HEAD_2);
String icon;
String searchUrl;
switch (PreferenceManager.getInstance().getSearchChoice()) {
switch (mPreferenceManager.getSearchChoice()) {
case 0:
// CUSTOM SEARCH
icon = "file:///android_asset/lightning.png";
searchUrl = PreferenceManager.getInstance().getSearchUrl();
searchUrl = mPreferenceManager.getSearchUrl();
break;
case 1:
// GOOGLE_SEARCH;
@@ -88,14 +127,14 @@ public class StartPage {
break;
case 5:
// STARTPAGE_SEARCH;
icon = "file:///android_asset/startpage.png";
// "https://startpage.com/graphics/startp_logo.gif";
icon = "file:///android_asset/png";
// "https://com/graphics/startp_logo.gif";
searchUrl = Constants.STARTPAGE_SEARCH;
break;
case 6:
// STARTPAGE_MOBILE
icon = "file:///android_asset/startpage.png";
// "https://startpage.com/graphics/startp_logo.gif";
icon = "file:///android_asset/png";
// "https://com/graphics/startp_logo.gif";
searchUrl = Constants.STARTPAGE_MOBILE_SEARCH;
break;
case 7:
@@ -131,13 +170,14 @@ public class StartPage {
}
homepageBuilder.append(icon);
homepageBuilder.append(StartPage.MIDDLE);
homepageBuilder.append(MIDDLE);
homepageBuilder.append(searchUrl);
homepageBuilder.append(StartPage.END);
homepageBuilder.append(END);
File homepage = new File(activity.getFilesDir(), StartPage.FILENAME);
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) {
@@ -148,4 +188,9 @@ public class StartPage {
return Constants.FILE + homepage;
}
public void load() {
executeOnExecutor(BrowserApp.getIOThread());
}
}
@@ -1,61 +0,0 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.controller;
import android.app.Activity;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Message;
import android.view.View;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient.CustomViewCallback;
import android.webkit.WebView;
import acr.browser.lightning.view.LightningView;
public interface BrowserController {
void updateUrl(String title, boolean shortUrl);
void updateProgress(int n);
void updateHistory(String title, String url);
void openFileChooser(ValueCallback<Uri> uploadMsg);
void updateTabs();
void onLongPress();
void onShowCustomView(View view, CustomViewCallback callback);
void onHideCustomView();
Bitmap getDefaultVideoPoster();
View getVideoLoadingProgressView();
void onCreateWindow(Message resultMsg);
void onCloseWindow(LightningView view);
Activity getActivity();
void hideActionBar();
void showActionBar();
void longClickPage(String url);
void openBookmarkPage(WebView view);
void showFileChooser(ValueCallback<Uri[]> filePathCallback);
void closeEmptyTab();
boolean proxyIsNotReady();
void updateBookmarkIndicator(String url);
}
@@ -0,0 +1,67 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.controller;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Message;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient.CustomViewCallback;
import acr.browser.lightning.activity.TabsManager;
import acr.browser.lightning.view.LightningView;
public interface UIController {
void changeToolbarBackground(@NonNull Bitmap favicon, @Nullable Drawable drawable);
@ColorInt
int getUiColor();
boolean getUseDarkTheme();
void updateUrl(@Nullable String title, boolean shortUrl);
void updateProgress(int n);
void updateHistory(@Nullable String title, @NonNull String url);
void openFileChooser(ValueCallback<Uri> uploadMsg);
void onShowCustomView(View view, CustomViewCallback callback);
void onShowCustomView(View view, CustomViewCallback callback, int requestedOrienation);
void onHideCustomView();
void onCreateWindow(Message resultMsg);
void onCloseWindow(LightningView view);
void hideActionBar();
void showActionBar();
void showFileChooser(ValueCallback<Uri[]> filePathCallback);
void closeEmptyTab();
void showCloseDialog(int position);
void newTabClicked();
void setForwardButtonEnabled(boolean enabled);
void setBackButtonEnabled(boolean enabled);
void tabChanged(LightningView tab);
TabsManager getTabModel();
}
@@ -0,0 +1,206 @@
package acr.browser.lightning.database;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import acr.browser.lightning.react.Action;
import acr.browser.lightning.react.Observable;
import acr.browser.lightning.react.Subscriber;
import acr.browser.lightning.utils.Utils;
public class BookmarkLocalSync {
private static final String TAG = BookmarkLocalSync.class.getSimpleName();
private static final String STOCK_BOOKMARKS_CONTENT = "content://browser/bookmarks";
private static final String CHROME_BOOKMARKS_CONTENT = "content://com.android.chrome.browser/bookmarks";
private static final String CHROME_BETA_BOOKMARKS_CONTENT = "content://com.chrome.beta.browser/bookmarks";
private static final String CHROME_DEV_BOOKMARKS_CONTENT = "content://com.chrome.dev.browser/bookmarks";
private static final String COLUMN_TITLE = "title";
private static final String COLUMN_URL = "url";
private static final String COLUMN_BOOKMARK = "bookmark";
@NonNull private final Context mContext;
public enum Source {
STOCK,
CHROME_STABLE,
CHROME_BETA,
CHROME_DEV
}
public BookmarkLocalSync(@NonNull Context context) {
mContext = context;
}
public List<HistoryItem> getBookmarksFromContentUri(String contentUri) {
List<HistoryItem> list = new ArrayList<>();
Cursor cursor = getBrowserCursor(contentUri);
try {
if (cursor != null) {
for (int n = 0; n < cursor.getColumnCount(); n++) {
Log.d(TAG, cursor.getColumnName(n));
}
while (cursor.moveToNext()) {
if (cursor.getInt(2) == 1) {
String url = cursor.getString(0);
String title = cursor.getString(1);
if (url.isEmpty()) {
continue;
}
if (title == null || title.isEmpty()) {
title = Utils.getDomainName(url);
}
if (title != null) {
list.add(new HistoryItem(url, title));
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
Utils.close(cursor);
return list;
}
@Nullable
@WorkerThread
private Cursor getBrowserCursor(String contentUri) {
Cursor cursor;
Uri uri = Uri.parse(contentUri);
try {
cursor = mContext.getContentResolver().query(uri,
new String[]{COLUMN_URL, COLUMN_TITLE, COLUMN_BOOKMARK}, null, null, null);
} catch (IllegalArgumentException e) {
return null;
}
return cursor;
}
@NonNull
public Observable<List<Source>> getSupportedBrowsers() {
return Observable.create(new Action<List<Source>>() {
@Override
public void onSubscribe(@NonNull Subscriber<List<Source>> subscriber) {
List<Source> sources = new ArrayList<>(1);
if (isBrowserSupported(STOCK_BOOKMARKS_CONTENT)) {
sources.add(Source.STOCK);
}
if (isBrowserSupported(CHROME_BOOKMARKS_CONTENT)) {
sources.add(Source.CHROME_STABLE);
}
if (isBrowserSupported(CHROME_BETA_BOOKMARKS_CONTENT)) {
sources.add(Source.CHROME_BETA);
}
if (isBrowserSupported(CHROME_DEV_BOOKMARKS_CONTENT)) {
sources.add(Source.CHROME_DEV);
}
subscriber.onNext(sources);
subscriber.onComplete();
}
});
}
private boolean isBrowserSupported(String contentUri) {
Cursor cursor = getBrowserCursor(contentUri);
boolean supported = cursor != null;
Utils.close(cursor);
return supported;
}
@NonNull
@WorkerThread
public List<HistoryItem> getBookmarksFromStockBrowser() {
return getBookmarksFromContentUri(STOCK_BOOKMARKS_CONTENT);
}
@NonNull
@WorkerThread
public List<HistoryItem> getBookmarksFromChrome() {
return getBookmarksFromContentUri(CHROME_BOOKMARKS_CONTENT);
}
@NonNull
@WorkerThread
public List<HistoryItem> getBookmarksFromChromeBeta() {
return getBookmarksFromContentUri(CHROME_BETA_BOOKMARKS_CONTENT);
}
@NonNull
@WorkerThread
public List<HistoryItem> 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;
}
@Nullable
@WorkerThread
private Cursor getChromeBetaCursor() {
return getBrowserCursor(CHROME_BETA_BOOKMARKS_CONTENT);
}
@Nullable
@WorkerThread
private Cursor getChromeDevCursor() {
return getBrowserCursor(CHROME_DEV_BOOKMARKS_CONTENT);
}
@Nullable
@WorkerThread
private Cursor getChromeCursor() {
return getBrowserCursor(CHROME_BOOKMARKS_CONTENT);
}
@Nullable
@WorkerThread
private Cursor getStockCursor() {
return getBrowserCursor(STOCK_BOOKMARKS_CONTENT);
}
public void printAllColumns() {
printColumns(CHROME_BETA_BOOKMARKS_CONTENT);
printColumns(CHROME_BOOKMARKS_CONTENT);
printColumns(CHROME_DEV_BOOKMARKS_CONTENT);
printColumns(STOCK_BOOKMARKS_CONTENT);
}
public void printColumns(String contentProvider) {
Cursor cursor = null;
Log.e(TAG, contentProvider);
Uri uri = Uri.parse(contentProvider);
try {
cursor = mContext.getContentResolver().query(uri, null, null, null, null);
} catch (Exception e) {
Log.e(TAG, "Error Occurred", e);
}
if (cursor != null) {
for (int n = 0; n < cursor.getColumnCount(); n++) {
Log.d(TAG, cursor.getColumnName(n));
}
cursor.close();
}
}
}
@@ -2,10 +2,10 @@ package acr.browser.lightning.database;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.os.Environment;
import android.provider.Browser;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
@@ -13,82 +13,194 @@ 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.Iterator;
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.preference.PreferenceManager;
import acr.browser.lightning.utils.Utils;
@Singleton
public class BookmarkManager {
private final Context mContext;
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";
private Set<String> mBookmarkSearchSet = new HashSet<>();
private final List<HistoryItem> mBookmarkList = new ArrayList<>();
private String mCurrentFolder = "";
private static BookmarkManager mInstance;
public static BookmarkManager getInstance(Context context) {
if (mInstance == null) {
mInstance = new BookmarkManager(context);
}
return mInstance;
}
@NonNull private final String DEFAULT_BOOKMARK_TITLE;
private BookmarkManager(Context context) {
mContext = context;
mBookmarkList.clear();
mBookmarkList.addAll(getAllBookmarks(true));
mBookmarkSearchSet = getBookmarkUrls(mBookmarkList);
}
private Map<String, HistoryItem> mBookmarksMap;
@NonNull private String mCurrentFolder = "";
@NonNull private final ExecutorService mExecutor;
private File mFilesDir;
public boolean isBookmark(String url) {
return mBookmarkSearchSet.contains(url);
@Inject
public BookmarkManager(@NonNull Context context) {
mExecutor = Executors.newSingleThreadExecutor();
DEFAULT_BOOKMARK_TITLE = context.getString(R.string.untitled);
mExecutor.execute(new BookmarkInitializer(context));
}
/**
* This method adds the the HistoryItem item to permanent bookmark storage.
* It returns true if the operation was successful.
* 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;
public BookmarkInitializer(Context context) {
mContext = context;
}
@Override
public void run() {
synchronized (BookmarkManager.this) {
mFilesDir = mContext.getFilesDir();
final Map<String, HistoryItem> 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<HistoryItem> mBookmarks;
public BookmarksWriter(List<HistoryItem> 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.<br>
* 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(HistoryItem item) {
File bookmarksFile = new File(mContext.getFilesDir(), FILE_BOOKMARKS);
if (item.getUrl() == null || mBookmarkSearchSet.contains(item.getUrl())) {
public synchronized boolean addBookmark(@NonNull HistoryItem item) {
final String url = item.getUrl();
if (mBookmarksMap.containsKey(url)) {
return false;
}
mBookmarkList.add(item);
BufferedWriter bookmarkWriter = null;
try {
bookmarkWriter = new BufferedWriter(new FileWriter(bookmarksFile, true));
JSONObject object = new JSONObject();
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();
mBookmarkSearchSet.add(item.getUrl());
} catch (IOException | JSONException e) {
e.printStackTrace();
} finally {
Utils.close(bookmarkWriter);
}
mBookmarksMap.put(url, item);
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
return true;
}
@@ -97,29 +209,17 @@ public class BookmarkManager {
*
* @param list the list of HistoryItems to add to bookmarks
*/
private synchronized void addBookmarkList(List<HistoryItem> list) {
File bookmarksFile = new File(mContext.getFilesDir(), FILE_BOOKMARKS);
BufferedWriter bookmarkWriter = null;
try {
bookmarkWriter = new BufferedWriter(new FileWriter(bookmarksFile, true));
JSONObject object = new JSONObject();
for (HistoryItem item : list) {
if (item.getUrl() != null && !mBookmarkSearchSet.contains(item.getUrl())) {
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();
mBookmarkSearchSet.add(item.getUrl());
mBookmarkList.add(item);
}
}
} catch (IOException | JSONException e) {
e.printStackTrace();
} finally {
Utils.close(bookmarkWriter);
public synchronized void addBookmarkList(@Nullable List<HistoryItem> 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())));
}
/**
@@ -128,13 +228,12 @@ public class BookmarkManager {
*
* @param deleteItem the bookmark item to delete
*/
public synchronized boolean deleteBookmark(HistoryItem deleteItem) {
public synchronized boolean deleteBookmark(@Nullable HistoryItem deleteItem) {
if (deleteItem == null || deleteItem.isFolder()) {
return false;
}
mBookmarkSearchSet.remove(deleteItem.getUrl());
mBookmarkList.remove(deleteItem);
overwriteBookmarks(mBookmarkList);
mBookmarksMap.remove(deleteItem.getUrl());
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
return true;
}
@@ -148,15 +247,15 @@ public class BookmarkManager {
if (newName.isEmpty()) {
return;
}
for (int n = 0; n < mBookmarkList.size(); n++) {
if (mBookmarkList.get(n).getFolder().equals(oldName)) {
mBookmarkList.get(n).setFolder(newName);
} else if (mBookmarkList.get(n).isFolder() && mBookmarkList.get(n).getTitle().equals(oldName)) {
mBookmarkList.get(n).setTitle(newName);
mBookmarkList.get(n).setUrl(Constants.FOLDER + newName);
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);
}
}
overwriteBookmarks(mBookmarkList);
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
}
/**
@@ -165,16 +264,32 @@ public class BookmarkManager {
* @param name the name of the folder to be deleted
*/
public synchronized void deleteFolder(@NonNull String name) {
Iterator<HistoryItem> iterator = mBookmarkList.iterator();
while (iterator.hasNext()) {
HistoryItem item = iterator.next();
if (!item.isFolder() && item.getFolder().equals(name)) {
item.setFolder("");
} else if (item.getTitle().equals(name)) {
iterator.remove();
final Map<String, HistoryItem> 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);
}
}
overwriteBookmarks(mBookmarkList);
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())));
}
/**
@@ -183,32 +298,32 @@ public class BookmarkManager {
* @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(HistoryItem oldItem, HistoryItem newItem) {
public synchronized void editBookmark(@Nullable HistoryItem oldItem, @Nullable HistoryItem newItem) {
if (oldItem == null || newItem == null || oldItem.isFolder()) {
return;
}
mBookmarkList.remove(oldItem);
mBookmarkList.add(newItem);
if (!oldItem.getUrl().equals(newItem.getUrl())) {
// Update the BookmarkMap if the URL has been changed
mBookmarkSearchSet.remove(oldItem.getUrl());
mBookmarkSearchSet.add(newItem.getUrl());
}
if (newItem.getUrl().isEmpty()) {
deleteBookmark(oldItem);
return;
}
if (newItem.getTitle().isEmpty()) {
newItem.setTitle(mContext.getString(R.string.untitled));
newItem.setTitle(DEFAULT_BOOKMARK_TITLE);
}
overwriteBookmarks(mBookmarkList);
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(Activity activity) {
public synchronized void exportBookmarks(@NonNull Activity activity) {
List<HistoryItem> bookmarkList = getAllBookmarks(true);
File bookmarksExport = new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
@@ -222,6 +337,7 @@ public class BookmarkManager {
}
BufferedWriter bookmarkWriter = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
bookmarkWriter = new BufferedWriter(new FileWriter(bookmarksExport,
false));
JSONObject object = new JSONObject();
@@ -235,7 +351,7 @@ public class BookmarkManager {
}
Utils.showSnackbar(activity, activity.getString(R.string.bookmark_export_path)
+ ' ' + bookmarksExport.getPath());
} catch (IOException | JSONException e) {
} catch (@NonNull IOException | JSONException e) {
e.printStackTrace();
} finally {
Utils.close(bookmarkWriter);
@@ -248,30 +364,12 @@ public class BookmarkManager {
* 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<HistoryItem> getAllBookmarks(boolean sort) {
List<HistoryItem> bookmarks = new ArrayList<>();
File bookmarksFile = new File(mContext.getFilesDir(), FILE_BOOKMARKS);
BufferedReader bookmarksReader = null;
try {
bookmarksReader = new BufferedReader(new FileReader(bookmarksFile));
String line;
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));
item.setImageId(R.drawable.ic_bookmark);
bookmarks.add(item);
}
} catch (IOException | JSONException e) {
e.printStackTrace();
} finally {
Utils.close(bookmarksReader);
}
final List<HistoryItem> bookmarks = new ArrayList<>(mBookmarksMap.values());
if (sort) {
Collections.sort(bookmarks, new SortIgnoreCase());
}
@@ -287,16 +385,47 @@ public class BookmarkManager {
* @param folder the name of the folder to retrieve bookmarks from
* @return a list of bookmarks found in that folder
*/
public synchronized List<HistoryItem> getBookmarksFromFolder(String folder, boolean sort) {
List<HistoryItem> bookmarks = new ArrayList<>();
@NonNull
public synchronized List<HistoryItem> getBookmarksFromFolder(@Nullable String folder, boolean sort) {
List<HistoryItem> bookmarks = new ArrayList<>(1);
if (folder == null || folder.isEmpty()) {
bookmarks.addAll(getFolders(sort));
folder = "";
}
mCurrentFolder = folder;
for (int n = 0; n < mBookmarkList.size(); n++) {
if (mBookmarkList.get(n).getFolder().equals(folder))
bookmarks.add(mBookmarkList.get(n));
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.
* <p/>
* 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<HistoryItem> getBookmarksCopyFromFolder(@Nullable String folder, boolean sort) {
List<HistoryItem> 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());
@@ -314,17 +443,13 @@ public class BookmarkManager {
}
/**
* Method is used internally for searching the bookmarks
* Returns the current folder
*
* @return a sorted map of all bookmarks, useful for seeing if a bookmark exists
* @return the current folder
*/
private Set<String> getBookmarkUrls(List<HistoryItem> list) {
Set<String> set = new HashSet<>();
for (int n = 0; n < list.size(); n++) {
if (!mBookmarkList.get(n).isFolder())
set.add(mBookmarkList.get(n).getUrl());
}
return set;
@Nullable
public String getCurrentFolder() {
return mCurrentFolder;
}
/**
@@ -334,35 +459,25 @@ public class BookmarkManager {
*
* @return a list of all folders
*/
public synchronized List<HistoryItem> getFolders(boolean sort) {
List<HistoryItem> folders = new ArrayList<>();
Set<String> folderMap = new HashSet<>();
File bookmarksFile = new File(mContext.getFilesDir(), FILE_BOOKMARKS);
BufferedReader bookmarksReader = null;
try {
bookmarksReader = new BufferedReader(new FileReader(bookmarksFile));
String line;
while ((line = bookmarksReader.readLine()) != null) {
JSONObject object = new JSONObject(line);
String folderName = object.getString(FOLDER);
if (!folderName.isEmpty() && !folderMap.contains(folderName)) {
HistoryItem item = new HistoryItem();
item.setTitle(folderName);
item.setUrl(Constants.FOLDER + folderName);
item.setIsFolder(true);
folderMap.add(folderName);
folders.add(item);
}
@NonNull
private synchronized List<HistoryItem> getFolders(boolean sort) {
final HashMap<String, HistoryItem> 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);
}
} catch (IOException | JSONException e) {
e.printStackTrace();
} finally {
Utils.close(bookmarksReader);
}
final List<HistoryItem> result = new ArrayList<>(folders.values());
if (sort) {
Collections.sort(folders, new SortIgnoreCase());
Collections.sort(result, new SortIgnoreCase());
}
return folders;
return result;
}
/**
@@ -371,28 +486,16 @@ public class BookmarkManager {
*
* @return a list of folder title strings
*/
@NonNull
public synchronized List<String> getFolderTitles() {
List<String> folders = new ArrayList<>();
Set<String> folderMap = new HashSet<>();
File bookmarksFile = new File(mContext.getFilesDir(), FILE_BOOKMARKS);
BufferedReader bookmarksReader = null;
try {
bookmarksReader = new BufferedReader(new FileReader(bookmarksFile));
String line;
while ((line = bookmarksReader.readLine()) != null) {
JSONObject object = new JSONObject(line);
String folderName = object.getString(FOLDER);
if (!folderName.isEmpty() && !folderMap.contains(folderName)) {
folders.add(folderName);
folderMap.add(folderName);
}
final Set<String> folders = new HashSet<>();
for (HistoryItem item : mBookmarksMap.values()) {
final String folderName = item.getFolder();
if (!folderName.isEmpty()) {
folders.add(folderName);
}
} catch (IOException | JSONException e) {
e.printStackTrace();
} finally {
Utils.close(bookmarksReader);
}
return folders;
return new ArrayList<>(folders);
}
/**
@@ -401,13 +504,14 @@ public class BookmarkManager {
*
* @param file the file to attempt to import bookmarks from
*/
public synchronized void importBookmarksFromFile(File file, Activity activity) {
public synchronized void importBookmarksFromFile(@Nullable File file, @NonNull Activity activity) {
if (file == null) {
return;
}
List<HistoryItem> list = new ArrayList<>();
BufferedReader bookmarksReader = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
bookmarksReader = new BufferedReader(new FileReader(file));
String line;
int number = 0;
@@ -424,7 +528,7 @@ public class BookmarkManager {
addBookmarkList(list);
String message = activity.getResources().getString(R.string.message_import);
Utils.showSnackbar(activity, number + " " + message);
} catch (IOException | JSONException e) {
} catch (@NonNull IOException | JSONException e) {
e.printStackTrace();
Utils.createInformativeDialog(activity, R.string.title_error, R.string.import_bookmark_error);
} finally {
@@ -432,60 +536,13 @@ public class BookmarkManager {
}
}
/**
* This method overwrites the entire bookmark file with the list of
* bookmarks. This is useful when an edit has been made to one or more
* bookmarks in the list
*
* @param list the list of bookmarks to overwrite the old ones with
*/
private synchronized void overwriteBookmarks(List<HistoryItem> list) {
File bookmarksFile = new File(mContext.getFilesDir(), FILE_BOOKMARKS);
BufferedWriter bookmarkWriter = null;
try {
bookmarkWriter = new BufferedWriter(new FileWriter(bookmarksFile, false));
JSONObject object = new JSONObject();
for (int n = 0; n < list.size(); n++) {
HistoryItem item = list.get(n);
if (!item.isFolder()) {
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();
}
}
} catch (IOException | JSONException e) {
e.printStackTrace();
} finally {
Utils.close(bookmarkWriter);
}
}
/**
* find the index of a bookmark in a list using only its URL
*
* @param list the list to search
* @param url the url to compare
* @return returns the index of the bookmark or -1 if none was found
*/
public static int getIndexOfBookmark(final List<HistoryItem> list, final String url) {
for (int n = 0; n < list.size(); n++) {
if (list.get(n).getUrl().equals(url)) {
return n;
}
}
return -1;
}
/**
* This class sorts bookmarks alphabetically, with folders coming after bookmarks
*/
public static class SortIgnoreCase implements Comparator<HistoryItem> {
private static class SortIgnoreCase implements Comparator<HistoryItem> {
public int compare(HistoryItem o1, HistoryItem o2) {
if (o1 == null || o2 == null || o1.getTitle() == null || o2.getTitle() == null) {
public int compare(@Nullable HistoryItem o1, @Nullable HistoryItem o2) {
if (o1 == null || o2 == null) {
return 0;
}
if (o1.isFolder() == o2.isFolder()) {
@@ -498,17 +555,4 @@ public class BookmarkManager {
}
}
private static final String[] DEV = {"https://twitter.com/RestainoAnthony", "The Developer"};
private static final String[] FACEBOOK = {"https://www.facebook.com/", "Facebook"};
private static final String[] TWITTER = {"https://twitter.com", "Twitter"};
private static final String[] GOOGLE = {"https://www.google.com/", "Google"};
private static final String[] YAHOO = {"https://www.yahoo.com/", "Yahoo"};
public static final String[][] DEFAULT_BOOKMARKS = {
DEV,
FACEBOOK,
TWITTER,
GOOGLE,
YAHOO
};
}
@@ -8,11 +8,19 @@ import android.content.Context;
import android.database.Cursor;
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 acr.browser.lightning.R;
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
@@ -20,7 +28,7 @@ public class HistoryDatabase extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = 2;
// Database Name
public static final String DATABASE_NAME = "historyManager";
private static final String DATABASE_NAME = "historyManager";
// HistoryItems table name
private static final String TABLE_HISTORY = "history";
@@ -31,25 +39,28 @@ public class HistoryDatabase extends SQLiteOpenHelper {
private static final String KEY_TITLE = "title";
private static final String KEY_TIME_VISITED = "time";
private SQLiteDatabase mDatabase;
@Nullable private SQLiteDatabase mDatabase;
private static HistoryDatabase mInstance;
public static HistoryDatabase getInstance(Context context) {
if (mInstance == null || mInstance.isClosed()) {
mInstance = new HistoryDatabase(context);
}
return mInstance;
@Inject
public HistoryDatabase(@NonNull Context context) {
super(context.getApplicationContext(), DATABASE_NAME, null, DATABASE_VERSION);
initialize();
}
private HistoryDatabase(Context context) {
super(context.getApplicationContext(), DATABASE_NAME, null, DATABASE_VERSION);
mDatabase = this.getWritableDatabase();
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(SQLiteDatabase db) {
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" + ')';
@@ -58,50 +69,59 @@ public class HistoryDatabase extends SQLiteOpenHelper {
// Upgrading database
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
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 void deleteHistory() {
public synchronized void deleteHistory() {
mDatabase = openIfNecessary();
mDatabase.delete(TABLE_HISTORY, null, null);
mDatabase.close();
mDatabase = this.getWritableDatabase();
}
public boolean isClosed() {
return mDatabase == null || !mDatabase.isOpen();
}
@Override
public synchronized void close() {
if (mDatabase != null) {
mDatabase.close();
mDatabase = null;
}
super.close();
}
public synchronized void deleteHistoryItem(String url) {
mDatabase.delete(TABLE_HISTORY, KEY_URL + " = ?", new String[] { url });
@NonNull
private SQLiteDatabase openIfNecessary() {
if (mDatabase == null || !mDatabase.isOpen()) {
mDatabase = this.getWritableDatabase();
}
return mDatabase;
}
public synchronized void visitHistoryItem(String url, String title) {
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);
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");
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 });
mDatabase.update(TABLE_HISTORY, values, KEY_URL + " = ?", new String[]{url});
} else {
addHistoryItem(new HistoryItem(url, title));
addHistoryItem(new HistoryItem(url, title == null ? "" : title));
}
q.close();
}
private synchronized void addHistoryItem(HistoryItem item) {
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());
@@ -109,9 +129,11 @@ public class HistoryDatabase extends SQLiteOpenHelper {
mDatabase.insert(TABLE_HISTORY, null, values);
}
String getHistoryItem(String url) {
Cursor cursor = mDatabase.query(TABLE_HISTORY, new String[] { KEY_ID, KEY_URL, KEY_TITLE },
KEY_URL + " = ?", new String[] { url }, null, null, null, null);
@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();
@@ -122,8 +144,13 @@ public class HistoryDatabase extends SQLiteOpenHelper {
return m;
}
public List<HistoryItem> findItemsContaining(String search) {
List<HistoryItem> itemList = new ArrayList<>();
@NonNull
public synchronized List<HistoryItem> findItemsContaining(@Nullable String search) {
mDatabase = openIfNecessary();
List<HistoryItem> itemList = new ArrayList<>(5);
if (search == null) {
return itemList;
}
String selectQuery = "SELECT * FROM " + TABLE_HISTORY + " WHERE " + KEY_TITLE + " LIKE '%"
+ search + "%' OR " + KEY_URL + " LIKE '%" + search + "%' " + "ORDER BY "
+ KEY_TIME_VISITED + " DESC LIMIT 5";
@@ -133,7 +160,6 @@ public class HistoryDatabase extends SQLiteOpenHelper {
if (cursor.moveToFirst()) {
do {
HistoryItem item = new HistoryItem();
item.setID(Integer.parseInt(cursor.getString(0)));
item.setUrl(cursor.getString(1));
item.setTitle(cursor.getString(2));
item.setImageId(R.drawable.ic_history);
@@ -145,8 +171,10 @@ public class HistoryDatabase extends SQLiteOpenHelper {
return itemList;
}
public List<HistoryItem> getLastHundredItems() {
List<HistoryItem> itemList = new ArrayList<>();
@NonNull
public synchronized List<HistoryItem> getLastHundredItems() {
mDatabase = openIfNecessary();
List<HistoryItem> itemList = new ArrayList<>(100);
String selectQuery = "SELECT * FROM " + TABLE_HISTORY + " ORDER BY " + KEY_TIME_VISITED
+ " DESC";
@@ -155,7 +183,6 @@ public class HistoryDatabase extends SQLiteOpenHelper {
if (cursor.moveToFirst()) {
do {
HistoryItem item = new HistoryItem();
item.setID(Integer.parseInt(cursor.getString(0)));
item.setUrl(cursor.getString(1));
item.setTitle(cursor.getString(2));
item.setImageId(R.drawable.ic_history);
@@ -167,7 +194,9 @@ public class HistoryDatabase extends SQLiteOpenHelper {
return itemList;
}
public List<HistoryItem> getAllHistoryItems() {
@NonNull
public synchronized List<HistoryItem> getAllHistoryItems() {
mDatabase = openIfNecessary();
List<HistoryItem> itemList = new ArrayList<>();
String selectQuery = "SELECT * FROM " + TABLE_HISTORY + " ORDER BY " + KEY_TIME_VISITED
+ " DESC";
@@ -177,7 +206,6 @@ public class HistoryDatabase extends SQLiteOpenHelper {
if (cursor.moveToFirst()) {
do {
HistoryItem item = new HistoryItem();
item.setID(Integer.parseInt(cursor.getString(0)));
item.setUrl(cursor.getString(1));
item.setTitle(cursor.getString(2));
item.setImageId(R.drawable.ic_history);
@@ -188,22 +216,12 @@ public class HistoryDatabase extends SQLiteOpenHelper {
return itemList;
}
public synchronized int updateHistoryItem(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());
return mDatabase.update(TABLE_HISTORY, values, KEY_ID + " = ?",
new String[] { String.valueOf(item.getId()) });
}
public int getHistoryItemsCount() {
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;
}
}
@@ -5,25 +5,32 @@ package acr.browser.lightning.database;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import acr.browser.lightning.utils.Preconditions;
public class HistoryItem implements Comparable<HistoryItem> {
// private variables
private int mId = 0;
@NonNull
private String mUrl = "";
@NonNull
private String mTitle = "";
@NonNull
private String mFolder = "";
@Nullable
private Bitmap mBitmap = null;
private int mImageId = 0;
private int mOrder = 0;
private boolean mIsFolder = false;
// Empty constructor
public HistoryItem() {
public HistoryItem() {}
}
public HistoryItem(HistoryItem item) {
public HistoryItem(@NonNull HistoryItem item) {
this.mUrl = item.mUrl;
this.mTitle = item.mTitle;
this.mFolder = item.mFolder;
@@ -31,43 +38,27 @@ public class HistoryItem implements Comparable<HistoryItem> {
this.mIsFolder = item.mIsFolder;
}
// constructor
public HistoryItem(int id, String url, String title) {
this.mId = id;
public HistoryItem(@NonNull String url, @NonNull String title) {
Preconditions.checkNonNull(url);
Preconditions.checkNonNull(title);
this.mUrl = url;
this.mTitle = title;
this.mBitmap = null;
}
// constructor
public HistoryItem(String url, String title) {
this.mUrl = url;
this.mTitle = title;
this.mBitmap = null;
}
// constructor
public HistoryItem(String url, String title, int imageId) {
public HistoryItem(@NonNull String url, @NonNull String title, int imageId) {
Preconditions.checkNonNull(url);
Preconditions.checkNonNull(title);
this.mUrl = url;
this.mTitle = title;
this.mBitmap = null;
this.mImageId = imageId;
}
// getting ID
public int getId() {
return this.mId;
}
public int getImageId() {
return this.mImageId;
}
// setting id
public void setID(int id) {
this.mId = id;
}
public void setImageId(int id) {
this.mImageId = id;
}
@@ -76,7 +67,7 @@ public class HistoryItem implements Comparable<HistoryItem> {
mBitmap = image;
}
public void setFolder(String folder) {
public void setFolder(@Nullable String folder) {
mFolder = (folder == null) ? "" : folder;
}
@@ -88,31 +79,31 @@ public class HistoryItem implements Comparable<HistoryItem> {
return mOrder;
}
@NonNull
public String getFolder() {
return mFolder;
}
@Nullable
public Bitmap getBitmap() {
return mBitmap;
}
// getting name
@NonNull
public String getUrl() {
return this.mUrl;
}
// setting name
public void setUrl(String url) {
public void setUrl(@Nullable String url) {
this.mUrl = (url == null) ? "" : url;
}
// getting phone number
@NonNull
public String getTitle() {
return this.mTitle;
}
// setting phone number
public void setTitle(String title) {
public void setTitle(@Nullable String title) {
this.mTitle = (title == null) ? "" : title;
}
@@ -124,6 +115,7 @@ public class HistoryItem implements Comparable<HistoryItem> {
return mIsFolder;
}
@NonNull
@Override
public String toString() {
return mTitle;
@@ -131,40 +123,34 @@ public class HistoryItem implements Comparable<HistoryItem> {
@Override
public int compareTo(@NonNull HistoryItem another) {
return mTitle.compareTo(another.mTitle);
int compare = this.mTitle.compareTo(another.mTitle);
if (compare == 0) {
return this.mUrl.compareTo(another.mUrl);
}
return compare;
}
@Override
public boolean equals(Object o) {
public boolean equals(@Nullable Object object) {
if (this == o) {
return true;
}
if (o == null || ((Object) this).getClass() != o.getClass()) {
return false;
}
if (this == object) return true;
if (object == null) return false;
if (!(object instanceof HistoryItem)) return false;
HistoryItem that = (HistoryItem) o;
HistoryItem that = (HistoryItem) object;
if (mId != that.mId) {
return false;
}
if (mImageId != that.mImageId) {
return false;
}
if (mBitmap != null ? !mBitmap.equals(that.mBitmap) : that.mBitmap != null) {
return false;
}
return mTitle.equals(that.mTitle) && mUrl.equals(that.mUrl);
return mImageId == that.mImageId &&
this.mTitle.equals(that.mTitle) && this.mUrl.equals(that.mUrl) &&
this.mFolder.equals(that.mFolder);
}
@Override
public int hashCode() {
int result = mId;
result = 31 * result + mUrl.hashCode();
int result = mUrl.hashCode();
result = 31 * result + mImageId;
result = 31 * result + mTitle.hashCode();
result = 31 * result + (mBitmap != null ? mBitmap.hashCode() : 0);
result = 32 * result + mFolder.hashCode();
result = 31 * result + mImageId;
return result;
@@ -0,0 +1,305 @@
package acr.browser.lightning.dialog;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.EditText;
import android.widget.LinearLayout;
import com.squareup.otto.Bus;
import java.util.List;
import javax.inject.Inject;
import acr.browser.lightning.R;
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.database.HistoryItem;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.Utils;
/**
* TODO Rename this class it doesn't build dialogs only for bookmarks
* <p/>
* Created by Stefano Pacifici on 02/09/15, based on Anthony C. Restaino's code.
*/
public class LightningDialogBuilder {
@Inject BookmarkManager mBookmarkManager;
@Inject PreferenceManager mPreferenceManager;
@Inject HistoryDatabase mHistoryDatabase;
@Inject Bus mEventBus;
@Inject
public LightningDialogBuilder() {
BrowserApp.getAppComponent().inject(this);
}
/**
* 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
*/
public void showLongPressedDialogForBookmarkUrl(@NonNull final Context context, @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
final Uri uri = Uri.parse(url);
final String filename = uri.getLastPathSegment();
final String folderTitle = filename.substring(0, filename.length() - BookmarkPage.FILENAME.length() - 1);
item = new HistoryItem();
item.setIsFolder(true);
item.setTitle(folderTitle);
item.setImageId(R.drawable.ic_folder);
item.setUrl(Constants.FOLDER + folderTitle);
} else {
item = mBookmarkManager.findBookmarkForUrl(url);
}
if (item != null) {
if (item.isFolder()) {
showBookmarkFolderLongPressedDialog(context, item);
} else {
showLongPressedDialogForBookmarkUrl(context, item);
}
}
}
public void showLongPressedDialogForBookmarkUrl(@NonNull final Context context, @NonNull final HistoryItem item) {
final DialogInterface.OnClickListener dialogClickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
mEventBus.post(new BrowserEvents.OpenUrlInNewTab(item.getUrl()));
break;
case DialogInterface.BUTTON_NEGATIVE:
if (mBookmarkManager.deleteBookmark(item)) {
mEventBus.post(new BookmarkEvents.Deleted(item));
}
break;
case DialogInterface.BUTTON_NEUTRAL:
showEditBookmarkDialog(context, item);
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.action_bookmarks)
.setMessage(R.string.dialog_bookmark)
.setCancelable(true)
.setPositiveButton(R.string.action_new_tab, dialogClickListener)
.setNegativeButton(R.string.action_delete, dialogClickListener)
.setNeutralButton(R.string.action_edit, dialogClickListener)
.show();
}
private void showEditBookmarkDialog(@NonNull final Context context, @NonNull final HistoryItem item) {
final AlertDialog.Builder editBookmarkDialog = new AlertDialog.Builder(context);
editBookmarkDialog.setTitle(R.string.title_edit_bookmark);
final View dialogLayout = View.inflate(context, R.layout.dialog_edit_bookmark, null);
final EditText getTitle = (EditText) dialogLayout.findViewById(R.id.bookmark_title);
getTitle.setText(item.getTitle());
final EditText getUrl = (EditText) dialogLayout.findViewById(R.id.bookmark_url);
getUrl.setText(item.getUrl());
final AutoCompleteTextView getFolder =
(AutoCompleteTextView) dialogLayout.findViewById(R.id.bookmark_folder);
getFolder.setHint(R.string.folder);
getFolder.setText(item.getFolder());
final List<String> folders = mBookmarkManager.getFolderTitles();
final ArrayAdapter<String> suggestionsAdapter = new ArrayAdapter<>(context,
android.R.layout.simple_dropdown_item_1line, folders);
getFolder.setThreshold(1);
getFolder.setAdapter(suggestionsAdapter);
editBookmarkDialog.setView(dialogLayout);
editBookmarkDialog.setPositiveButton(context.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);
mEventBus.post(new BookmarkEvents.BookmarkChanged(item, editedItem));
}
});
editBookmarkDialog.show();
}
public void showBookmarkFolderLongPressedDialog(@NonNull final Context context, @NonNull final HistoryItem item) {
// assert item.isFolder();
final DialogInterface.OnClickListener dialogClickListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
showRenameFolderDialog(context, item);
break;
case DialogInterface.BUTTON_NEGATIVE:
mBookmarkManager.deleteFolder(item.getTitle());
// setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false);
mEventBus.post(new BookmarkEvents.Deleted(item));
break;
}
}
};
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.action_folder)
.setMessage(R.string.dialog_folder)
.setCancelable(true)
.setPositiveButton(R.string.action_rename, dialogClickListener)
.setNegativeButton(R.string.action_delete, dialogClickListener)
.show();
}
private void showRenameFolderDialog(@NonNull final Context context, @NonNull final HistoryItem item) {
// assert item.isFolder();
final AlertDialog.Builder editFolderDialog = new AlertDialog.Builder(context);
editFolderDialog.setTitle(R.string.title_rename_folder);
final EditText getTitle = new EditText(context);
getTitle.setHint(R.string.hint_title);
getTitle.setText(item.getTitle());
getTitle.setSingleLine();
LinearLayout layout = new LinearLayout(context);
layout.setOrientation(LinearLayout.VERTICAL);
int padding = Utils.dpToPx(10);
layout.setPadding(padding, padding, padding, padding);
layout.addView(getTitle);
editFolderDialog.setView(layout);
editFolderDialog.setPositiveButton(context.getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
final String oldTitle = item.getTitle();
final String newTitle = getTitle.getText().toString();
final HistoryItem editedItem = new HistoryItem();
editedItem.setTitle(newTitle);
editedItem.setUrl(Constants.FOLDER + newTitle);
editedItem.setFolder(item.getFolder());
editedItem.setIsFolder(true);
mBookmarkManager.renameFolder(oldTitle, newTitle);
mEventBus.post(new BookmarkEvents.BookmarkChanged(item, editedItem));
}
});
editFolderDialog.show();
}
public void showLongPressedHistoryLinkDialog(final Context context, @NonNull final String url) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url));
break;
case DialogInterface.BUTTON_NEGATIVE:
mHistoryDatabase.deleteHistoryItem(url);
// openHistory();
mEventBus.post(new BrowserEvents.OpenHistoryInCurrentTab());
break;
case DialogInterface.BUTTON_NEUTRAL:
mEventBus.post(new BrowserEvents.OpenUrlInCurrentTab(url));
break;
default:
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.action_history)
.setMessage(R.string.dialog_history_long_press)
.setCancelable(true)
.setPositiveButton(R.string.action_new_tab, dialogClickListener)
.setNegativeButton(R.string.action_delete, dialogClickListener)
.setNeutralButton(R.string.action_open, dialogClickListener)
.show();
}
// 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,
@NonNull final String userAgent) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url));
break;
case DialogInterface.BUTTON_NEGATIVE:
mEventBus.post(new BrowserEvents.OpenUrlInCurrentTab(url));
break;
case DialogInterface.BUTTON_NEUTRAL:
Utils.downloadFile(activity, mPreferenceManager, url, userAgent, "attachment");
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(url.replace(Constants.HTTP, ""))
.setCancelable(true)
.setMessage(R.string.dialog_image)
.setPositiveButton(R.string.action_new_tab, dialogClickListener)
.setNegativeButton(R.string.action_open, dialogClickListener)
.setNeutralButton(R.string.action_download, dialogClickListener)
.show();
}
public void showLongPressLinkDialog(@NonNull final Context context, final String url) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url));
break;
case DialogInterface.BUTTON_NEGATIVE:
mEventBus.post(new BrowserEvents.OpenUrlInCurrentTab(url));
break;
case DialogInterface.BUTTON_NEUTRAL:
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("label", url);
clipboard.setPrimaryClip(clip);
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(context); // dialog
builder.setTitle(url)
.setCancelable(true)
.setMessage(R.string.dialog_link)
.setPositiveButton(R.string.action_new_tab, dialogClickListener)
.setNegativeButton(R.string.action_open, dialogClickListener)
.setNeutralButton(R.string.action_copy, dialogClickListener)
.show();
}
}
@@ -3,46 +3,61 @@
*/
package acr.browser.lightning.download;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
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.webkit.CookieManager;
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.bus.BrowserEvents;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.Utils;
/**
* Handle download requests
*/
public class DownloadHandler {
private static final String LOGTAG = "DLHandler";
private static final String TAG = DownloadHandler.class.getSimpleName();
private static final String COOKIE_REQUEST_HEADER = "Cookie";
public static final String DEFAULT_DOWNLOAD_PATH =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
.getPath();
/**
* Notify the host application a download should be done, or that the data
* should be streamed if a streaming viewer is available.
*
* @param activity Activity requesting the download.
* @param context The context in which the download was requested.
* @param url The full url to the content that should be downloaded
* @param userAgent User agent of the downloading application.
* @param contentDisposition Content-disposition http header, if present.
* @param mimetype The mimetype of the content reported by the server
*/
public static void onDownloadStart(Activity activity, String url, String userAgent,
String contentDisposition, String mimetype) {
public static void onDownloadStart(@NonNull Context context, @NonNull PreferenceManager manager, String url, String userAgent,
@Nullable String contentDisposition, String mimetype) {
// if we're dealing wih A/V content that's not explicitly marked
// for download, check if it's streamable.
if (contentDisposition == null
@@ -52,18 +67,22 @@ public class DownloadHandler {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(url), mimetype);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ResolveInfo info = activity.getPackageManager().resolveActivity(intent,
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
intent.setSelector(null);
}
ResolveInfo info = context.getPackageManager().resolveActivity(intent,
PackageManager.MATCH_DEFAULT_ONLY);
if (info != null) {
ComponentName myName = activity.getComponentName();
// If we resolved to ourselves, we don't want to attempt to
// load the url only to try and download it again.
if (!myName.getPackageName().equals(info.activityInfo.packageName)
|| !myName.getClassName().equals(info.activityInfo.name)) {
if (BuildConfig.APPLICATION_ID.equals(info.activityInfo.packageName)
|| MainActivity.class.getName().equals(info.activityInfo.name)) {
// someone (other than us) knows how to handle this mime
// type with this scheme, don't download.
try {
activity.startActivity(intent);
context.startActivity(intent);
return;
} catch (ActivityNotFoundException ex) {
// Best behavior is to fall back to a download in this
@@ -72,14 +91,14 @@ public class DownloadHandler {
}
}
}
onDownloadStartNoStream(activity, url, userAgent, contentDisposition, mimetype
);
onDownloadStartNoStream(context, manager, url, userAgent, contentDisposition, mimetype);
}
// This is to work around the fact that java.net.URI throws Exceptions
// instead of just encoding URL's properly
// Helper method for onDownloadStartNoStream
private static String encodePath(String path) {
@NonNull
private static String encodePath(@NonNull String path) {
char[] chars = path.toCharArray();
boolean needed = false;
@@ -110,17 +129,18 @@ public class DownloadHandler {
* Notify the host application a download should be done, even if there is a
* streaming viewer available for thise type.
*
* @param activity Activity requesting the download.
* @param context The context in which the download is requested.
* @param url The full url to the content that should be downloaded
* @param userAgent User agent of the downloading application.
* @param contentDisposition Content-disposition http header, if present.
* @param mimetype The mimetype of the content reported by the server
*/
/* package */
private static void onDownloadStartNoStream(final Activity activity, String url, String userAgent,
String contentDisposition, String mimetype) {
String filename = URLUtil.guessFileName(url, contentDisposition, mimetype);
private static void onDownloadStartNoStream(@NonNull final Context 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
String status = Environment.getExternalStorageState();
@@ -130,14 +150,14 @@ public class DownloadHandler {
// Check to see if the SDCard is busy, same as the music app
if (status.equals(Environment.MEDIA_SHARED)) {
msg = activity.getString(R.string.download_sdcard_busy_dlg_msg);
msg = context.getString(R.string.download_sdcard_busy_dlg_msg);
title = R.string.download_sdcard_busy_dlg_title;
} else {
msg = activity.getString(R.string.download_no_sdcard_dlg_msg, filename);
msg = context.getString(R.string.download_no_sdcard_dlg_msg);
title = R.string.download_no_sdcard_dlg_title;
}
new AlertDialog.Builder(activity).setTitle(title)
new AlertDialog.Builder(context).setTitle(title)
.setIcon(android.R.drawable.ic_dialog_alert).setMessage(msg)
.setPositiveButton(R.string.action_ok, null).show();
return;
@@ -152,7 +172,8 @@ public class DownloadHandler {
} catch (Exception e) {
// This only happens for very bad urls, we want to catch the
// exception here
Log.e(LOGTAG, "Exception while trying to parse url '" + url + '\'', e);
Log.e(TAG, "Exception while trying to parse url '" + url + '\'', e);
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.problem_download));
return;
}
@@ -162,7 +183,7 @@ public class DownloadHandler {
try {
request = new DownloadManager.Request(uri);
} catch (IllegalArgumentException e) {
Utils.showSnackbar(activity, R.string.cannot_download);
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.cannot_download));
return;
}
request.setMimeType(mimetype);
@@ -170,41 +191,157 @@ public class DownloadHandler {
// or, should it be set to one of several Environment.DIRECTORY* dirs
// depending on mimetype?
String location = PreferenceManager.getInstance().getDownloadDirectory();
request.setDestinationInExternalPublicDir(location, filename);
String location = preferences.getDownloadDirectory();
Uri downloadFolder;
location = addNecessarySlashes(location);
downloadFolder = Uri.parse(location);
File dir = new File(downloadFolder.getPath());
if (!dir.isDirectory() && !dir.mkdirs()) {
// Cannot make the directory
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.problem_location_download));
return;
}
if (!isWriteAccessAvailable(downloadFolder)) {
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.problem_location_download));
return;
}
request.setDestinationUri(Uri.parse(Constants.FILE + location + filename));
// let this downloaded file be scanned by MediaScanner - so that it can
// show up in Gallery app, for example.
request.setVisibleInDownloadsUi(true);
request.allowScanningByMediaScanner();
request.setDescription(webAddress.getHost());
// XXX: Have to use the old url since the cookies were stored using the
// old percent-encoded url.
String cookies = CookieManager.getInstance().getCookie(url);
request.addRequestHeader("cookie", cookies);
request.addRequestHeader(COOKIE_REQUEST_HEADER, cookies);
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
if (mimetype == null) {
Log.d(TAG, "Mimetype is null");
if (TextUtils.isEmpty(addressString)) {
return;
}
// We must have long pressed on a link or image to download it. We
// are not sure of the mimetype in this case, so do a head request
new FetchUrlMimeType(activity, request, addressString, cookies, userAgent).start();
new FetchUrlMimeType(context, request, addressString, cookies, userAgent).start();
} else {
final DownloadManager manager = (DownloadManager) activity
Log.d(TAG, "Valid mimetype, attempting to download");
final DownloadManager manager = (DownloadManager) context
.getSystemService(Context.DOWNLOAD_SERVICE);
new Thread("Browser download") {
@Override
public void run() {
try {
manager.enqueue(request);
} catch (IllegalArgumentException e) {
// Probably got a bad URL or something
e.printStackTrace();
Utils.showSnackbar(activity, R.string.cannot_download);
}
}
}.start();
Utils.showSnackbar(activity, R.string.download_pending);
try {
manager.enqueue(request);
} catch (IllegalArgumentException e) {
// Probably got a bad URL or something
Log.e(TAG, "Unable to enqueue request", e);
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.cannot_download));
} catch (SecurityException e) {
// TODO write a download utility that downloads files rather than rely on the system
// because the system can only handle Environment.getExternal... as a path
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.problem_location_download));
}
eventBus.post(new BrowserEvents.ShowSnackBarMessage(
context.getString(R.string.download_pending) + ' ' + filename));
}
}
private static final String sFileName = "test";
private static final String sFileExtension = ".txt";
/**
* Determine whether there is write access in the given directory. Returns false if a
* file cannot be created in the directory or if the directory does not exist.
*
* @param directory the directory to check for write access
* @return returns true if the directory can be written to or is in a directory that can
* be written to. false if there is no write access.
*/
public static boolean isWriteAccessAvailable(@Nullable String directory) {
if (directory == null || directory.isEmpty()) {
return false;
}
String dir = addNecessarySlashes(directory);
dir = getFirstRealParentDirectory(dir);
File file = new File(dir + sFileName + sFileExtension);
for (int n = 0; n < 100; n++) {
if (!file.exists()) {
try {
if (file.createNewFile()) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
return true;
} catch (IOException ignored) {
return false;
}
} else {
file = new File(dir + sFileName + '-' + n + sFileExtension);
}
}
return file.canWrite();
}
/**
* Returns the first parent directory of a directory that exists. This is useful
* for subdirectories that do not exist but their parents do.
*
* @param directory the directory to find the first existent parent
* @return the first existent parent
*/
@Nullable
private static String getFirstRealParentDirectory(@Nullable String directory) {
while (true) {
if (directory == null || directory.isEmpty()) {
return "/";
}
directory = addNecessarySlashes(directory);
File file = new File(directory);
if (!file.isDirectory()) {
int indexSlash = directory.lastIndexOf('/');
if (indexSlash > 0) {
String parent = directory.substring(0, indexSlash);
int previousIndex = parent.lastIndexOf('/');
if (previousIndex > 0) {
directory = parent.substring(0, previousIndex);
} else {
return "/";
}
} else {
return "/";
}
} else {
return directory;
}
}
}
private static boolean isWriteAccessAvailable(@NonNull Uri fileUri) {
File file = new File(fileUri.getPath());
try {
if (file.createNewFile()) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
return true;
} catch (IOException ignored) {
return false;
}
}
@NonNull
public static String addNecessarySlashes(@Nullable String originalPath) {
if (originalPath == null || originalPath.length() == 0) {
return "/";
}
if (originalPath.charAt(originalPath.length() - 1) != '/') {
originalPath = originalPath + '/';
}
if (originalPath.charAt(0) != '/') {
originalPath = '/' + originalPath;
}
return originalPath;
}
}
@@ -3,19 +3,24 @@
*/
package acr.browser.lightning.download;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.Context;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.webkit.MimeTypeMap;
import android.webkit.URLUtil;
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.utils.Utils;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.bus.BrowserEvents;
/**
* This class is used to pull down the http headers of a given URL so that we
@@ -25,7 +30,7 @@ import acr.browser.lightning.utils.Utils;
* just clicks on the link, we will do the same steps of correcting the mimetype
* down in android.os.webkit.LoadListener rather than handling it here.
*/
public class FetchUrlMimeType extends Thread {
class FetchUrlMimeType extends Thread {
private final Context mContext;
@@ -37,20 +42,20 @@ public class FetchUrlMimeType extends Thread {
private final String mUserAgent;
public FetchUrlMimeType(Activity activity, DownloadManager.Request request, String uri,
public FetchUrlMimeType(Context context, DownloadManager.Request request, String uri,
String cookies, String userAgent) {
mContext = activity.getApplicationContext();
mContext = context;
mRequest = request;
mUri = uri;
mCookies = cookies;
mUserAgent = userAgent;
Utils.showSnackbar(activity, R.string.download_pending);
}
@Override
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;
@@ -79,7 +84,7 @@ public class FetchUrlMimeType extends Thread {
contentDisposition = contentDispositionHeader;
}
}
} catch (IllegalArgumentException | IOException ex) {
} catch (@NonNull IllegalArgumentException | IOException ex) {
if (connection != null)
connection.disconnect();
} finally {
@@ -87,6 +92,7 @@ public class FetchUrlMimeType extends Thread {
connection.disconnect();
}
String filename = "";
if (mimeType != null) {
if (mimeType.equalsIgnoreCase("text/plain")
|| mimeType.equalsIgnoreCase("application/octet-stream")) {
@@ -96,7 +102,7 @@ public class FetchUrlMimeType extends Thread {
mRequest.setMimeType(newMimeType);
}
}
String filename = URLUtil.guessFileName(mUri, contentDisposition, mimeType);
filename = URLUtil.guessFileName(mUri, contentDisposition, mimeType);
mRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename);
}
@@ -104,5 +110,13 @@ public class FetchUrlMimeType extends Thread {
DownloadManager manager = (DownloadManager) mContext
.getSystemService(Context.DOWNLOAD_SERVICE);
manager.enqueue(mRequest);
Handler handler = new Handler(Looper.getMainLooper());
final String file = filename;
handler.post(new Runnable() {
@Override
public void run() {
eventBus.post(new BrowserEvents.ShowSnackBarMessage(mContext.getString(R.string.download_pending) + ' ' + file));
}
});
}
}
@@ -3,6 +3,7 @@
*/
package acr.browser.lightning.download;
import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.support.v7.app.AlertDialog;
@@ -11,43 +12,64 @@ 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.preference.PreferenceManager;
import com.anthonycr.grant.PermissionsManager;
import com.anthonycr.grant.PermissionsResultAction;
import javax.inject.Inject;
public class LightningDownloadListener implements DownloadListener {
private final Activity mActivity;
public LightningDownloadListener(Activity activity) {
mActivity = activity;
@Inject PreferenceManager mPreferenceManager;
public LightningDownloadListener(Activity context) {
BrowserApp.getAppComponent().inject(this);
mActivity = context;
}
@Override
public void onDownloadStart(final String url, final String userAgent,
final String contentDisposition, final String mimetype, long contentLength) {
String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype);
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
DownloadHandler.onDownloadStart(mActivity, url, userAgent,
contentDisposition, mimetype);
break;
final String contentDisposition, final String mimetype, 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);
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
DownloadHandler.onDownloadStart(mActivity, mPreferenceManager, url, userAgent,
contentDisposition, mimetype);
break;
case DialogInterface.BUTTON_NEGATIVE:
break;
}
}
};
case DialogInterface.BUTTON_NEGATIVE:
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); // dialog
builder.setTitle(fileName)
.setMessage(mActivity.getResources().getString(R.string.dialog_download))
.setPositiveButton(mActivity.getResources().getString(R.string.action_download),
dialogClickListener)
.setNegativeButton(mActivity.getResources().getString(R.string.action_cancel),
dialogClickListener).show();
Log.i(Constants.TAG, "Downloading" + fileName);
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); // dialog
builder.setTitle(fileName)
.setMessage(mActivity.getResources().getString(R.string.dialog_download))
.setPositiveButton(mActivity.getResources().getString(R.string.action_download),
dialogClickListener)
.setNegativeButton(mActivity.getResources().getString(R.string.action_cancel),
dialogClickListener).show();
Log.i(Constants.TAG, "Downloading" + fileName);
}
@Override
public void onDenied(String permission) {
//TODO show message
}
});
}
}
@@ -3,6 +3,9 @@
*/
package acr.browser.lightning.download;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -11,39 +14,39 @@ import static android.util.Patterns.GOOD_IRI_CHAR;
/**
* Web Address Parser
*
* <p/>
* This is called WebAddress, rather than URL or URI, because it attempts to
* parse the stuff that a user will actually type into a browser address widget.
*
* <p/>
* Unlike java.net.uri, this parser will not choke on URIs missing schemes. It
* will only throw a ParseException if the input is really hosed.
*
* <p/>
* If given an https scheme but no port, fills in port
*/
public class WebAddress {
class WebAddress {
private String mScheme;
private String mHost;
private int mPort;
private String mPath;
private String mAuthInfo;
static final int MATCH_GROUP_SCHEME = 1;
static final int MATCH_GROUP_AUTHORITY = 2;
static final int MATCH_GROUP_HOST = 3;
static final int MATCH_GROUP_PORT = 4;
static final int MATCH_GROUP_PATH = 5;
static final Pattern sAddressPattern = Pattern.compile(
/* scheme */"(?:(http|https|file)\\:\\/\\/)?" +
/* authority */"(?:([-A-Za-z0-9$_.+!*'(),;?&=]+(?:\\:[-A-Za-z0-9$_.+!*'(),;?&=]+)?)@)?" +
private static final int MATCH_GROUP_SCHEME = 1;
private static final int MATCH_GROUP_AUTHORITY = 2;
private static final int MATCH_GROUP_HOST = 3;
private static final int MATCH_GROUP_PORT = 4;
private static final int MATCH_GROUP_PATH = 5;
private static final Pattern sAddressPattern = Pattern.compile(
/* scheme */"(?:(http|https|file)://)?" +
/* authority */"(?:([-A-Za-z0-9$_.+!*'(),;?&=]+(?::[-A-Za-z0-9$_.+!*'(),;?&=]+)?)@)?" +
/* host */"([" + GOOD_IRI_CHAR + "%_-][" + GOOD_IRI_CHAR + "%_\\.-]*|\\[[0-9a-fA-F:\\.]+\\])?" +
/* port */"(?:\\:([0-9]*))?" +
/* path */"(\\/?[^#]*)?" +
/* port */"(?::([0-9]*))?" +
/* path */"(/?[^#]*)?" +
/* anchor */".*", Pattern.CASE_INSENSITIVE);
/**
* Parses given URI-like string.
*/
public WebAddress(String address) {
public WebAddress(@Nullable String address) throws IllegalArgumentException {
if (address == null) {
throw new IllegalArgumentException("address can't be null");
@@ -111,6 +114,7 @@ public class WebAddress {
}
}
@NonNull
@Override
public String toString() {
@@ -134,7 +138,7 @@ public class WebAddress {
return mScheme;
}
public void setHost(String host) {
public void setHost(@NonNull String host) {
mHost = host;
}
@@ -8,7 +8,7 @@ import android.content.DialogInterface;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import java.util.Arrays;
@@ -16,9 +16,8 @@ import java.util.List;
import acr.browser.lightning.R;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.preference.PreferenceManager;
public class AdvancedSettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
public class AdvancedSettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
private static final String SETTINGS_NEWWINDOW = "allow_new_window";
private static final String SETTINGS_ENABLECOOKIES = "allow_cookies";
@@ -29,7 +28,6 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
private static final String SETTINGS_TEXTENCODING = "text_encoding";
private Activity mActivity;
private PreferenceManager mPreferences;
private CheckBoxPreference cbAllowPopups, cbenablecookies, cbcookiesInkognito, cbrestoreTabs;
private Preference renderingmode, urlcontent, textEncoding;
private CharSequence[] mUrlOptions;
@@ -46,8 +44,6 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
}
private void initPrefs() {
// mPreferences storage
mPreferences = PreferenceManager.getInstance();
renderingmode = findPreference(SETTINGS_RENDERINGMODE);
textEncoding = findPreference(SETTINGS_TEXTENCODING);
@@ -65,7 +61,7 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
cbcookiesInkognito.setOnPreferenceChangeListener(this);
cbrestoreTabs.setOnPreferenceChangeListener(this);
switch (mPreferences.getRenderingMode()) {
switch (mPreferenceManager.getRenderingMode()) {
case 0:
renderingmode.setSummary(getString(R.string.name_normal));
break;
@@ -78,22 +74,25 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
case 3:
renderingmode.setSummary(getString(R.string.name_inverted_grayscale));
break;
case 4:
renderingmode.setSummary(getString(R.string.name_increase_contrast));
break;
}
textEncoding.setSummary(mPreferences.getTextEncoding());
textEncoding.setSummary(mPreferenceManager.getTextEncoding());
mUrlOptions = getResources().getStringArray(R.array.url_content_array);
int option = mPreferences.getUrlBoxContentChoice();
int option = mPreferenceManager.getUrlBoxContentChoice();
urlcontent.setSummary(mUrlOptions[option]);
cbAllowPopups.setChecked(mPreferences.getPopupsEnabled());
cbenablecookies.setChecked(mPreferences.getCookiesEnabled());
cbcookiesInkognito.setChecked(mPreferences.getIncognitoCookiesEnabled());
cbrestoreTabs.setChecked(mPreferences.getRestoreLostTabsEnabled());
cbAllowPopups.setChecked(mPreferenceManager.getPopupsEnabled());
cbenablecookies.setChecked(mPreferenceManager.getCookiesEnabled());
cbcookiesInkognito.setChecked(mPreferenceManager.getIncognitoCookiesEnabled());
cbrestoreTabs.setChecked(mPreferenceManager.getRestoreLostTabsEnabled());
}
@Override
public boolean onPreferenceClick(Preference preference) {
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_RENDERINGMODE:
renderPicker();
@@ -110,23 +109,23 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
// switch preferences
switch (preference.getKey()) {
case SETTINGS_NEWWINDOW:
mPreferences.setPopupsEnabled((Boolean) newValue);
mPreferenceManager.setPopupsEnabled((Boolean) newValue);
cbAllowPopups.setChecked((Boolean) newValue);
return true;
case SETTINGS_ENABLECOOKIES:
mPreferences.setCookiesEnabled((Boolean) newValue);
mPreferenceManager.setCookiesEnabled((Boolean) newValue);
cbenablecookies.setChecked((Boolean) newValue);
return true;
case SETTINGS_COOKIESINKOGNITO:
mPreferences.setIncognitoCookiesEnabled((Boolean) newValue);
mPreferenceManager.setIncognitoCookiesEnabled((Boolean) newValue);
cbcookiesInkognito.setChecked((Boolean) newValue);
return true;
case SETTINGS_RESTORETABS:
mPreferences.setRestoreLostTabsEnabled((Boolean) newValue);
mPreferenceManager.setRestoreLostTabsEnabled((Boolean) newValue);
cbrestoreTabs.setChecked((Boolean) newValue);
return true;
default:
@@ -140,14 +139,15 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
CharSequence[] chars = {mActivity.getString(R.string.name_normal),
mActivity.getString(R.string.name_inverted),
mActivity.getString(R.string.name_grayscale),
mActivity.getString(R.string.name_inverted_grayscale)};
mActivity.getString(R.string.name_inverted_grayscale),
mActivity.getString(R.string.name_increase_contrast)};
int n = mPreferences.getRenderingMode();
int n = mPreferenceManager.getRenderingMode();
picker.setSingleChoiceItems(chars, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setRenderingMode(which);
mPreferenceManager.setRenderingMode(which);
switch (which) {
case 0:
renderingmode.setSummary(getString(R.string.name_normal));
@@ -161,16 +161,13 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
case 3:
renderingmode.setSummary(getString(R.string.name_inverted_grayscale));
break;
case 4:
renderingmode.setSummary(getString(R.string.name_increase_contrast));
break;
}
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
picker.show();
}
@@ -178,22 +175,16 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.text_encoding));
final List<String> textEncodingList = Arrays.asList(Constants.TEXT_ENCODINGS);
int n = textEncodingList.indexOf(mPreferences.getTextEncoding());
int n = textEncodingList.indexOf(mPreferenceManager.getTextEncoding());
picker.setSingleChoiceItems(Constants.TEXT_ENCODINGS, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setTextEncoding(Constants.TEXT_ENCODINGS[which]);
mPreferenceManager.setTextEncoding(Constants.TEXT_ENCODINGS[which]);
textEncoding.setSummary(Constants.TEXT_ENCODINGS[which]);
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
picker.show();
}
@@ -201,24 +192,18 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.url_contents));
int n = mPreferences.getUrlBoxContentChoice();
int n = mPreferenceManager.getUrlBoxContentChoice();
picker.setSingleChoiceItems(mUrlOptions, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setUrlBoxContentChoice(which);
mPreferenceManager.setUrlBoxContentChoice(which);
if (which < mUrlOptions.length) {
urlcontent.setSummary(mUrlOptions[which]);
}
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
picker.show();
}
}
@@ -6,84 +6,329 @@ package acr.browser.lightning.fragment;
import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.widget.ArrayAdapter;
import com.anthonycr.grant.PermissionsManager;
import com.anthonycr.grant.PermissionsResultAction;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import javax.inject.Inject;
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.utils.PermissionsManager;
import acr.browser.lightning.database.HistoryItem;
import acr.browser.lightning.react.OnSubscribe;
import acr.browser.lightning.react.Schedulers;
import acr.browser.lightning.utils.Preconditions;
import acr.browser.lightning.utils.Utils;
public class BookmarkSettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener {
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";
private static final String SETTINGS_DELETE_BOOKMARKS = "delete_bookmarks";
private Activity mActivity;
private BookmarkManager mBookmarkManager;
@Nullable private Activity mActivity;
@Inject BookmarkManager mBookmarkManager;
private File[] mFileList;
private String[] mFileNameList;
private PermissionsManager mPermissionsManager;
private static String[] REQUIRED_PERMISSIONS = new String[]{
@Nullable private BookmarkLocalSync mSync;
private static final String[] REQUIRED_PERMISSIONS = new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
private static final File mPath = new File(Environment.getExternalStorageDirectory().toString());
private class ImportBookmarksTask extends AsyncTask<Void, Void, Integer> {
@NonNull private final WeakReference<Activity> mActivityReference;
private final Source mSource;
public ImportBookmarksTask(Activity activity, Source source) {
mActivityReference = new WeakReference<>(activity);
mSource = source;
}
@Override
protected Integer doInBackground(Void... params) {
List<HistoryItem> list;
Log.d(Constants.TAG, "Loading bookmarks from: " + mSource.name());
switch (mSource) {
case STOCK:
list = getSync().getBookmarksFromStockBrowser();
break;
case CHROME_STABLE:
list = getSync().getBookmarksFromChrome();
break;
case CHROME_BETA:
list = getSync().getBookmarksFromChromeBeta();
break;
case CHROME_DEV:
list = getSync().getBookmarksFromChromeDev();
break;
default:
list = new ArrayList<>(0);
break;
}
int count = 0;
if (!list.isEmpty()) {
mBookmarkManager.addBookmarkList(list);
count = list.size();
}
return count;
}
@Override
protected void onPostExecute(Integer num) {
super.onPostExecute(num);
Activity activity = mActivityReference.get();
if (activity != null) {
int number = num;
final String message = activity.getResources().getString(R.string.message_import);
Utils.showSnackbar(activity, number + " " + message);
}
}
}
@NonNull
private BookmarkLocalSync getSync() {
Preconditions.checkNonNull(mActivity);
if (mSync == null) {
mSync = new BookmarkLocalSync(mActivity);
}
return mSync;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
BrowserApp.getAppComponent().inject(this);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preference_bookmarks);
mActivity = getActivity();
mBookmarkManager = BookmarkManager.getInstance(mActivity.getApplicationContext());
mSync = new BookmarkLocalSync(mActivity);
initPrefs();
mPermissionsManager = PermissionsManager.getInstance();
PermissionsManager permissionsManager = PermissionsManager.getInstance();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mPermissionsManager.requestPermissionsIfNecessary(getActivity(), REQUIRED_PERMISSIONS);
permissionsManager.requestPermissionsIfNecessaryForResult(getActivity(), REQUIRED_PERMISSIONS, null);
}
}
@Override
public void onDestroy() {
super.onDestroy();
mActivity = null;
}
private void initPrefs() {
Preference exportpref = findPreference(SETTINGS_EXPORT);
Preference importpref = findPreference(SETTINGS_IMPORT);
Preference exportPref = findPreference(SETTINGS_EXPORT);
Preference importPref = findPreference(SETTINGS_IMPORT);
Preference deletePref = findPreference(SETTINGS_DELETE_BOOKMARKS);
exportPref.setOnPreferenceClickListener(this);
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);
}
});
}
});
exportpref.setOnPreferenceClickListener(this);
importpref.setOnPreferenceClickListener(this);
}
@Override
public boolean onPreferenceClick(Preference preference) {
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_EXPORT:
if (PermissionsManager.checkPermissions(getActivity(), REQUIRED_PERMISSIONS)) {
mBookmarkManager.exportBookmarks(getActivity());
}
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(getActivity(), REQUIRED_PERMISSIONS,
new PermissionsResultAction() {
@Override
public void onGranted() {
mBookmarkManager.exportBookmarks(getActivity());
}
@Override
public void onDenied(String permission) {
//TODO Show message
}
});
return true;
case SETTINGS_IMPORT:
if (PermissionsManager.checkPermissions(getActivity(), REQUIRED_PERMISSIONS)) {
loadFileList(null);
createDialog();
}
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(getActivity(), REQUIRED_PERMISSIONS,
new PermissionsResultAction() {
@Override
public void onGranted() {
loadFileList(null);
createDialog();
}
@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<List<Source>>() {
@Override
public void onNext(@Nullable List<Source> items) {
Activity activity = getActivity();
if (items == null || activity == null) {
return;
}
List<String> titles = buildTitleList(activity, items);
showChooserDialog(activity, titles);
}
});
return true;
case SETTINGS_DELETE_BOOKMARKS:
showDeleteBookmarksDialog();
return true;
default:
return false;
}
}
private void loadFileList(File path) {
private void showDeleteBookmarksDialog() {
Activity activity = getActivity();
if (activity == null) {
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.action_delete);
builder.setMessage(R.string.action_delete_all_bookmarks);
builder.setNegativeButton(R.string.no, null);
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mBookmarkManager.deleteAllBookmarks();
}
});
builder.show();
}
@NonNull
private List<String> buildTitleList(@NonNull Activity activity, @NonNull List<Source> items) {
List<String> titles = new ArrayList<>();
String title;
for (Source source : items) {
switch (source) {
case STOCK:
titles.add(getString(R.string.stock_browser));
break;
case CHROME_STABLE:
title = getTitle(activity, "com.android.chrome");
if (title != null) {
titles.add(title);
}
break;
case CHROME_BETA:
title = getTitle(activity, "com.chrome.beta");
if (title != null) {
titles.add(title);
}
break;
case CHROME_DEV:
title = getTitle(activity, "com.chrome.beta");
if (title != null) {
titles.add(title);
}
break;
default:
break;
}
}
return titles;
}
private void showChooserDialog(final Activity activity, List<String> list) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
final ArrayAdapter<String> adapter = new ArrayAdapter<>(activity,
android.R.layout.simple_list_item_1);
for (String title : list) {
adapter.add(title);
}
builder.setTitle(R.string.supported_browsers_title);
builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String title = adapter.getItem(which);
Source source = null;
if (title.equals(getString(R.string.stock_browser))) {
source = Source.STOCK;
} else if (title.equals(getTitle(activity, "com.android.chrome"))) {
source = Source.CHROME_STABLE;
} else if (title.equals(getTitle(activity, "com.android.beta"))) {
source = Source.CHROME_BETA;
} else if (title.equals(getTitle(activity, "com.android.dev"))) {
source = Source.CHROME_DEV;
}
if (source != null) {
new ImportBookmarksTask(activity, source).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
});
builder.show();
}
@Nullable
private static String getTitle(@NonNull Activity activity, @NonNull String packageName) {
PackageManager pm = activity.getPackageManager();
try {
ApplicationInfo info = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
CharSequence title = pm.getApplicationLabel(info);
if (title != null) {
return title.toString();
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
private void loadFileList(@Nullable File path) {
File file;
if (path != null) {
file = path;
@@ -113,10 +358,10 @@ public class BookmarkSettingsFragment extends PreferenceFragment implements Pref
}
}
private class SortName implements Comparator<File> {
private static class SortName implements Comparator<File> {
@Override
public int compare(File a, File b) {
public int compare(@NonNull File a, @NonNull File b) {
if (a.isDirectory() && b.isDirectory())
return a.getName().compareTo(b.getName());
@@ -0,0 +1,414 @@
package acr.browser.lightning.fragment;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewCompat;
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.squareup.otto.Bus;
import com.squareup.otto.Subscribe;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.activity.ReadingActivity;
import acr.browser.lightning.activity.TabsManager;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.async.AsyncExecutor;
import acr.browser.lightning.bus.BookmarkEvents;
import acr.browser.lightning.bus.BrowserEvents;
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.dialog.LightningDialogBuilder;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.async.ImageDownloadTask;
import acr.browser.lightning.react.Action;
import acr.browser.lightning.react.Observable;
import acr.browser.lightning.react.OnSubscribe;
import acr.browser.lightning.react.Schedulers;
import acr.browser.lightning.react.Subscriber;
import acr.browser.lightning.utils.ThemeUtils;
import acr.browser.lightning.view.LightningView;
public class BookmarksFragment extends Fragment implements View.OnClickListener, View.OnLongClickListener {
private final static String TAG = BookmarksFragment.class.getSimpleName();
public final static String INCOGNITO_MODE = TAG + ".INCOGNITO_MODE";
// Managers
@Inject BookmarkManager mBookmarkManager;
// Event bus
@Inject Bus mEventBus;
// Dialog builder
@Inject LightningDialogBuilder mBookmarksDialogBuilder;
@Inject PreferenceManager mPreferenceManager;
private TabsManager mTabsManager;
// Adapter
private BookmarkViewAdapter mBookmarkAdapter;
// Preloaded images
private Bitmap mWebpageBitmap, mFolderBitmap;
// Bookmarks
private final List<HistoryItem> mBookmarks = new ArrayList<>();
// Views
private ListView mBookmarksListView;
private ImageView mBookmarkTitleImage, mBookmarkImage;
// Colors
private int mIconColor, mScrollIndex;
private boolean mIsIncognito;
private Observable<BookmarkViewAdapter> initBookmarkManager() {
return Observable.create(new Action<BookmarkViewAdapter>() {
@Override
public void onSubscribe(@NonNull Subscriber<BookmarkViewAdapter> subscriber) {
Context context = getContext();
if (context != null) {
mBookmarkAdapter = new BookmarkViewAdapter(context, mBookmarks);
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false);
subscriber.onNext(mBookmarkAdapter);
}
subscriber.onComplete();
}
});
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
BrowserApp.getAppComponent().inject(this);
final Bundle arguments = getArguments();
final Context context = getContext();
mTabsManager = ((UIController) context).getTabModel();
mIsIncognito = arguments.getBoolean(INCOGNITO_MODE, false);
boolean darkTheme = mPreferenceManager.getUseTheme() != 0 || mIsIncognito;
mWebpageBitmap = ThemeUtils.getThemedBitmap(context, R.drawable.ic_webpage, darkTheme);
mFolderBitmap = ThemeUtils.getThemedBitmap(context, R.drawable.ic_folder, darkTheme);
mIconColor = darkTheme ? ThemeUtils.getIconDarkThemeColor(context) :
ThemeUtils.getIconLightThemeColor(context);
}
// 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);
if (item.isFolder()) {
mScrollIndex = mBookmarksListView.getFirstVisiblePosition();
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(item.getTitle(), true), true);
} else {
mEventBus.post(new BrowserEvents.OpenUrlInCurrentTab(item.getUrl()));
}
}
};
private final OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
final HistoryItem item = mBookmarks.get(position);
handleLongPress(item);
return true;
}
};
@Override
public void onResume() {
super.onResume();
if (mBookmarkAdapter != null) {
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false);
}
}
@Nullable
@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);
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);
}
}
});
setupNavigationButton(view, R.id.action_add_bookmark, R.id.icon_star);
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<BookmarkViewAdapter>() {
@Override
public void onNext(@Nullable BookmarkViewAdapter item) {
mBookmarksListView.setAdapter(mBookmarkAdapter);
}
});
return view;
}
@Override
public void onStart() {
super.onStart();
mEventBus.register(this);
}
@Override
public void onStop() {
super.onStop();
mEventBus.unregister(this);
}
public void reinitializePreferences() {
Activity activity = getActivity();
if (activity == null) {
return;
}
boolean darkTheme = mPreferenceManager.getUseTheme() != 0 || mIsIncognito;
mWebpageBitmap = ThemeUtils.getThemedBitmap(activity, R.drawable.ic_webpage, darkTheme);
mFolderBitmap = ThemeUtils.getThemedBitmap(activity, R.drawable.ic_folder, darkTheme);
mIconColor = darkTheme ? ThemeUtils.getIconDarkThemeColor(activity) :
ThemeUtils.getIconLightThemeColor(activity);
}
@Subscribe
public void addBookmark(@NonNull final BrowserEvents.BookmarkAdded event) {
updateBookmarkIndicator(event.url);
String folder = mBookmarkManager.getCurrentFolder();
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(folder, true), false);
}
@Subscribe
public void currentPageInfo(@NonNull final BrowserEvents.CurrentPageUrl event) {
updateBookmarkIndicator(event.url);
String folder = mBookmarkManager.getCurrentFolder();
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(folder, true), false);
}
@Subscribe
public void bookmarkChanged(BookmarkEvents.BookmarkChanged event) {
String folder = mBookmarkManager.getCurrentFolder();
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(folder, true), false);
}
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);
}
}
@Subscribe
public void userPressedBack(final BrowserEvents.UserPressedBack event) {
if (mBookmarkManager.isRootFolder()) {
mEventBus.post(new BookmarkEvents.CloseBookmarks());
} else {
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), true);
mBookmarksListView.setSelection(mScrollIndex);
}
}
@Subscribe
public void bookmarkDeleted(@NonNull final BookmarkEvents.Deleted event) {
mBookmarks.remove(event.item);
if (event.item.isFolder()) {
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false);
} else {
mBookmarkAdapter.notifyDataSetChanged();
}
}
private void setBookmarkDataSet(@NonNull List<HistoryItem> items, boolean animate) {
mBookmarks.clear();
mBookmarks.addAll(items);
mBookmarkAdapter.notifyDataSetChanged();
final int resource;
if (mBookmarkManager.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);
} else {
mBookmarkTitleImage.setImageResource(resource);
}
}
private void setupNavigationButton(@NonNull View view, @IdRes int buttonId, @IdRes int imageId) {
FrameLayout frameButton = (FrameLayout) view.findViewById(buttonId);
frameButton.setOnClickListener(this);
frameButton.setOnLongClickListener(this);
ImageView buttonImage = (ImageView) view.findViewById(imageId);
buttonImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN);
}
private void handleLongPress(@NonNull final HistoryItem item) {
if (item.isFolder()) {
mBookmarksDialogBuilder.showBookmarkFolderLongPressedDialog(getContext(), item);
} else {
mBookmarksDialogBuilder.showLongPressedDialogForBookmarkUrl(getContext(), item);
}
}
@Override
public void onClick(@NonNull View v) {
switch (v.getId()) {
case R.id.action_add_bookmark:
mEventBus.post(new BookmarkEvents.ToggleBookmarkForCurrentPage());
break;
case R.id.action_reading:
LightningView currentTab = mTabsManager.getCurrentTab();
if (currentTab != null) {
Intent read = new Intent(getActivity(), ReadingActivity.class);
read.putExtra(Constants.LOAD_READING_URL, currentTab.getUrl());
startActivity(read);
}
break;
case R.id.action_toggle_desktop:
LightningView current = mTabsManager.getCurrentTab();
if (current != null) {
current.toggleDesktopUA(getActivity());
current.reload();
// TODO add back drawer closing
}
break;
default:
break;
}
}
@Override
public boolean onLongClick(View v) {
return false;
}
private class BookmarkViewAdapter extends ArrayAdapter<HistoryItem> {
final Context context;
public BookmarkViewAdapter(Context context, @NonNull List<HistoryItem> data) {
super(context, R.layout.bookmark_list_item, data);
this.context = context;
}
@Override
public View getView(int position, View convertView, 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();
}
ViewCompat.jumpDrawablesToCurrentState(row);
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, context)
.executeOnExecutor(AsyncExecutor.getInstance());
} else {
holder.favicon.setImageBitmap(web.getBitmap());
}
return row;
}
private class BookmarkViewHolder {
TextView txtTitle;
ImageView favicon;
}
}
}
@@ -0,0 +1,57 @@
package acr.browser.lightning.fragment;
import android.app.Activity;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.SwitchPreference;
import android.support.annotation.NonNull;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.Utils;
public class DebugSettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
private static final String LEAK_CANARY = "leak_canary_enabled";
@Inject PreferenceManager mPreferenceManager;
private SwitchPreference mSwitchLeakCanary;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
BrowserApp.getAppComponent().inject(this);
addPreferencesFromResource(R.xml.preference_debug);
mSwitchLeakCanary = (SwitchPreference) findPreference(LEAK_CANARY);
mSwitchLeakCanary.setChecked(mPreferenceManager.getUseLeakCanary());
mSwitchLeakCanary.setOnPreferenceChangeListener(this);
}
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
return false;
}
@Override
public boolean onPreferenceChange(@NonNull Preference preference, @NonNull Object newValue) {
switch (preference.getKey()) {
case LEAK_CANARY:
boolean value = Boolean.TRUE.equals(newValue);
mPreferenceManager.setUseLeakCanary(value);
Activity activity = getActivity();
if (activity != null) {
Utils.showSnackbar(activity, R.string.app_restart);
}
mSwitchLeakCanary.setChecked(value);
return true;
}
return false;
}
}
@@ -8,7 +8,7 @@ import android.content.DialogInterface;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.view.Gravity;
import android.view.LayoutInflater;
@@ -19,9 +19,8 @@ import android.widget.SeekBar;
import android.widget.TextView;
import acr.browser.lightning.R;
import acr.browser.lightning.preference.PreferenceManager;
public class DisplaySettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
public class DisplaySettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
private static final String SETTINGS_HIDESTATUSBAR = "fullScreenOption";
private static final String SETTINGS_FULLSCREEN = "fullscreen";
@@ -38,7 +37,6 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
private static final float XSMALL = 10.0f;
private Activity mActivity;
private PreferenceManager mPreferences;
private CheckBoxPreference cbstatus, cbfullscreen, cbviewport, cboverview, cbreflow;
private Preference theme;
private String[] mThemeOptions;
@@ -57,9 +55,8 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
private void initPrefs() {
// mPreferences storage
mPreferences = PreferenceManager.getInstance();
mThemeOptions = this.getResources().getStringArray(R.array.themes);
mCurrentTheme = mPreferences.getUseTheme();
mCurrentTheme = mPreferenceManager.getUseTheme();
theme = findPreference(SETTINGS_THEME);
Preference textsize = findPreference(SETTINGS_TEXTSIZE);
@@ -77,17 +74,17 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
cboverview.setOnPreferenceChangeListener(this);
cbreflow.setOnPreferenceChangeListener(this);
cbstatus.setChecked(mPreferences.getHideStatusBarEnabled());
cbfullscreen.setChecked(mPreferences.getFullScreenEnabled());
cbviewport.setChecked(mPreferences.getUseWideViewportEnabled());
cboverview.setChecked(mPreferences.getOverviewModeEnabled());
cbreflow.setChecked(mPreferences.getTextReflowEnabled());
cbstatus.setChecked(mPreferenceManager.getHideStatusBarEnabled());
cbfullscreen.setChecked(mPreferenceManager.getFullScreenEnabled());
cbviewport.setChecked(mPreferenceManager.getUseWideViewportEnabled());
cboverview.setChecked(mPreferenceManager.getOverviewModeEnabled());
cbreflow.setChecked(mPreferenceManager.getTextReflowEnabled());
theme.setSummary(mThemeOptions[mPreferences.getUseTheme()]);
theme.setSummary(mThemeOptions[mPreferenceManager.getUseTheme()]);
}
@Override
public boolean onPreferenceClick(Preference preference) {
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_THEME:
themePicker();
@@ -101,27 +98,27 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
// switch preferences
switch (preference.getKey()) {
case SETTINGS_HIDESTATUSBAR:
mPreferences.setHideStatusBarEnabled((Boolean) newValue);
mPreferenceManager.setHideStatusBarEnabled((Boolean) newValue);
cbstatus.setChecked((Boolean) newValue);
return true;
case SETTINGS_FULLSCREEN:
mPreferences.setFullScreenEnabled((Boolean) newValue);
mPreferenceManager.setFullScreenEnabled((Boolean) newValue);
cbfullscreen.setChecked((Boolean) newValue);
return true;
case SETTINGS_VIEWPORT:
mPreferences.setUseWideViewportEnabled((Boolean) newValue);
mPreferenceManager.setUseWideViewportEnabled((Boolean) newValue);
cbviewport.setChecked((Boolean) newValue);
return true;
case SETTINGS_OVERVIEWMODE:
mPreferences.setOverviewModeEnabled((Boolean) newValue);
mPreferenceManager.setOverviewModeEnabled((Boolean) newValue);
cboverview.setChecked((Boolean) newValue);
return true;
case SETTINGS_REFLOW:
mPreferences.setTextReflowEnabled((Boolean) newValue);
mPreferenceManager.setTextReflowEnabled((Boolean) newValue);
cbreflow.setChecked((Boolean) newValue);
return true;
default:
@@ -139,39 +136,24 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
sample.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT));
sample.setGravity(Gravity.CENTER_HORIZONTAL);
view.addView(sample);
bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar view, int size, boolean user) {
sample.setTextSize(getTextSize(size));
}
@Override
public void onStartTrackingTouch(SeekBar arg0) {
}
@Override
public void onStopTrackingTouch(SeekBar arg0) {
}
});
bar.setOnSeekBarChangeListener(new TextSeekBarListener(sample));
final int MAX = 5;
bar.setMax(MAX);
bar.setProgress(MAX - mPreferences.getTextSize());
bar.setProgress(MAX - mPreferenceManager.getTextSize());
builder.setView(view);
builder.setTitle(R.string.title_text_size);
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
mPreferences.setTextSize(MAX - bar.getProgress());
mPreferenceManager.setTextSize(MAX - bar.getProgress());
}
});
builder.show();
}
private float getTextSize(int size) {
private static float getTextSize(int size) {
switch (size) {
case 0:
return XSMALL;
@@ -194,12 +176,12 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.theme));
int n = mPreferences.getUseTheme();
int n = mPreferenceManager.getUseTheme();
picker.setSingleChoiceItems(mThemeOptions, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setUseTheme(which);
mPreferenceManager.setUseTheme(which);
if (which < mThemeOptions.length) {
theme.setSummary(mThemeOptions[which]);
}
@@ -210,7 +192,7 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
@Override
public void onClick(DialogInterface dialog, int which) {
if (mCurrentTheme != mPreferences.getUseTheme()) {
if (mCurrentTheme != mPreferenceManager.getUseTheme()) {
getActivity().onBackPressed();
}
}
@@ -218,11 +200,32 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
picker.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
if (mCurrentTheme != mPreferences.getUseTheme()) {
if (mCurrentTheme != mPreferenceManager.getUseTheme()) {
getActivity().onBackPressed();
}
}
});
picker.show();
}
private static class TextSeekBarListener implements SeekBar.OnSeekBarChangeListener {
private final TextView sample;
public TextSeekBarListener(TextView sample) {this.sample = sample;}
@Override
public void onProgressChanged(SeekBar view, int size, boolean user) {
this.sample.setTextSize(getTextSize(size));
}
@Override
public void onStartTrackingTouch(SeekBar arg0) {
}
@Override
public void onStopTrackingTouch(SeekBar arg0) {
}
}
}
@@ -5,30 +5,30 @@ package acr.browser.lightning.fragment;
import android.app.Activity;
import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
import android.text.InputFilter;
import android.util.Log;
import android.util.TypedValue;
import android.text.TextWatcher;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import acr.browser.lightning.R;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.download.DownloadHandler;
import acr.browser.lightning.utils.ProxyUtils;
import acr.browser.lightning.utils.ThemeUtils;
import acr.browser.lightning.utils.Utils;
public class GeneralSettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
public class GeneralSettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
private static final String SETTINGS_PROXY = "proxy";
private static final String SETTINGS_FLASH = "cb_flash";
@@ -45,13 +45,11 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
private Activity mActivity;
private static final int API = android.os.Build.VERSION.SDK_INT;
private PreferenceManager mPreferences;
private CharSequence[] mProxyChoices;
private Preference proxy, useragent, downloadloc, home, searchengine;
private String mDownloadLocation;
private int mAgentChoice;
private String mHomepage;
private CheckBoxPreference cbFlash, cbAds, cbImages, cbJsScript, cbColorMode, cbgooglesuggest, cbDrawerTabs;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -65,22 +63,19 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
}
private void initPrefs() {
// mPreferences storage
mPreferences = PreferenceManager.getInstance();
proxy = findPreference(SETTINGS_PROXY);
useragent = findPreference(SETTINGS_USERAGENT);
downloadloc = findPreference(SETTINGS_DOWNLOAD);
home = findPreference(SETTINGS_HOME);
searchengine = findPreference(SETTINGS_SEARCHENGINE);
cbFlash = (CheckBoxPreference) findPreference(SETTINGS_FLASH);
cbAds = (CheckBoxPreference) findPreference(SETTINGS_ADS);
cbImages = (CheckBoxPreference) findPreference(SETTINGS_IMAGES);
cbJsScript = (CheckBoxPreference) findPreference(SETTINGS_JAVASCRIPT);
cbColorMode = (CheckBoxPreference) findPreference(SETTINGS_COLORMODE);
cbgooglesuggest = (CheckBoxPreference) findPreference(SETTINGS_GOOGLESUGGESTIONS);
cbDrawerTabs = (CheckBoxPreference) findPreference(SETTINGS_DRAWERTABS);
CheckBoxPreference cbFlash = (CheckBoxPreference) findPreference(SETTINGS_FLASH);
CheckBoxPreference cbAds = (CheckBoxPreference) findPreference(SETTINGS_ADS);
CheckBoxPreference cbImages = (CheckBoxPreference) findPreference(SETTINGS_IMAGES);
CheckBoxPreference cbJsScript = (CheckBoxPreference) findPreference(SETTINGS_JAVASCRIPT);
CheckBoxPreference cbColorMode = (CheckBoxPreference) findPreference(SETTINGS_COLORMODE);
CheckBoxPreference cbgooglesuggest = (CheckBoxPreference) findPreference(SETTINGS_GOOGLESUGGESTIONS);
CheckBoxPreference cbDrawerTabs = (CheckBoxPreference) findPreference(SETTINGS_DRAWERTABS);
proxy.setOnPreferenceClickListener(this);
useragent.setOnPreferenceClickListener(this);
@@ -95,25 +90,25 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
cbgooglesuggest.setOnPreferenceChangeListener(this);
cbDrawerTabs.setOnPreferenceChangeListener(this);
mAgentChoice = mPreferences.getUserAgentChoice();
mHomepage = mPreferences.getHomepage();
mDownloadLocation = mPreferences.getDownloadDirectory();
mAgentChoice = mPreferenceManager.getUserAgentChoice();
mHomepage = mPreferenceManager.getHomepage();
mDownloadLocation = mPreferenceManager.getDownloadDirectory();
mProxyChoices = getResources().getStringArray(R.array.proxy_choices_array);
int choice = mPreferences.getProxyChoice();
int choice = mPreferenceManager.getProxyChoice();
if (choice == Constants.PROXY_MANUAL) {
proxy.setSummary(mPreferences.getProxyHost() + ':' + mPreferences.getProxyPort());
proxy.setSummary(mPreferenceManager.getProxyHost() + ':' + mPreferenceManager.getProxyPort());
} else {
proxy.setSummary(mProxyChoices[choice]);
}
if (API >= 19) {
mPreferences.setFlashSupport(0);
if (API >= Build.VERSION_CODES.KITKAT) {
mPreferenceManager.setFlashSupport(0);
}
setSearchEngineSummary(mPreferences.getSearchChoice());
setSearchEngineSummary(mPreferenceManager.getSearchChoice());
downloadloc.setSummary(Constants.EXTERNAL_STORAGE + '/' + mDownloadLocation);
downloadloc.setSummary(mDownloadLocation);
if (mHomepage.contains("about:home")) {
home.setSummary(getResources().getString(R.string.action_homepage));
@@ -139,28 +134,27 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
useragent.setSummary(getResources().getString(R.string.agent_custom));
}
int flashNum = mPreferences.getFlashSupport();
boolean imagesBool = mPreferences.getBlockImagesEnabled();
boolean enableJSBool = mPreferences.getJavaScriptEnabled();
int flashNum = mPreferenceManager.getFlashSupport();
boolean imagesBool = mPreferenceManager.getBlockImagesEnabled();
boolean enableJSBool = mPreferenceManager.getJavaScriptEnabled();
proxy.setEnabled(Constants.FULL_VERSION);
cbAds.setEnabled(Constants.FULL_VERSION);
cbFlash.setEnabled(API < 19);
cbFlash.setEnabled(API < Build.VERSION_CODES.KITKAT);
cbImages.setChecked(imagesBool);
cbJsScript.setChecked(enableJSBool);
cbFlash.setChecked(flashNum > 0);
cbAds.setChecked(Constants.FULL_VERSION && mPreferences.getAdBlockEnabled());
cbColorMode.setChecked(mPreferences.getColorModeEnabled());
cbgooglesuggest.setChecked(mPreferences.getGoogleSearchSuggestionsEnabled());
cbDrawerTabs.setChecked(mPreferences.getShowTabsInDrawer(true));
cbAds.setChecked(Constants.FULL_VERSION && mPreferenceManager.getAdBlockEnabled());
cbColorMode.setChecked(mPreferenceManager.getColorModeEnabled());
cbgooglesuggest.setChecked(mPreferenceManager.getGoogleSearchSuggestionsEnabled());
cbDrawerTabs.setChecked(mPreferenceManager.getShowTabsInDrawer(true));
}
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 = mPreferences.getSearchUrl();
String mSearchUrl = mPreferenceManager.getSearchUrl();
getSearchUrl.setText(mSearchUrl);
urlPicker.setView(getSearchUrl);
urlPicker.setPositiveButton(getResources().getString(R.string.action_ok),
@@ -168,7 +162,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
@Override
public void onClick(DialogInterface dialog, int which) {
String text = getSearchUrl.getText().toString();
mPreferences.setSearchUrl(text);
mPreferenceManager.setSearchUrl(text);
searchengine.setSummary(getResources().getString(R.string.custom_url) + ": "
+ text);
}
@@ -185,7 +179,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
mPreferences.setFlashSupport(1);
mPreferenceManager.setFlashSupport(1);
}
})
.setNegativeButton(getResources().getString(R.string.action_auto),
@@ -193,13 +187,13 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setFlashSupport(2);
mPreferenceManager.setFlashSupport(2);
}
}).setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
mPreferences.setFlashSupport(0);
mPreferenceManager.setFlashSupport(0);
}
});
@@ -210,7 +204,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
private void proxyChoicePicker() {
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.http_proxy));
picker.setSingleChoiceItems(mProxyChoices, mPreferences.getProxyChoice(),
picker.setSingleChoiceItems(mProxyChoices, mPreferenceManager.getProxyChoice(),
new DialogInterface.OnClickListener() {
@Override
@@ -218,30 +212,24 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
setProxyChoice(which);
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
picker.show();
}
private void setProxyChoice(int choice) {
ProxyUtils utils = ProxyUtils.getInstance(mActivity);
switch (choice) {
case Constants.PROXY_ORBOT:
choice = utils.setProxyChoice(choice, mActivity);
choice = ProxyUtils.setProxyChoice(choice, mActivity);
break;
case Constants.PROXY_I2P:
choice = utils.setProxyChoice(choice, mActivity);
choice = ProxyUtils.setProxyChoice(choice, mActivity);
break;
case Constants.PROXY_MANUAL:
manualProxyPicker();
break;
}
mPreferences.setProxyChoice(choice);
mPreferenceManager.setProxyChoice(choice);
if (choice < mProxyChoices.length)
proxy.setSummary(mProxyChoices[choice]);
}
@@ -259,8 +247,8 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
filterArray[0] = new InputFilter.LengthFilter(maxCharacters - 1);
eProxyPort.setFilters(filterArray);
eProxyHost.setText(mPreferences.getProxyHost());
eProxyPort.setText(Integer.toString(mPreferences.getProxyPort()));
eProxyHost.setText(mPreferenceManager.getProxyHost());
eProxyPort.setText(Integer.toString(mPreferenceManager.getProxyPort()));
new AlertDialog.Builder(mActivity)
.setTitle(R.string.manual_proxy)
@@ -275,10 +263,10 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
// larger than max integer
proxyPort = Integer.parseInt(eProxyPort.getText().toString());
} catch (NumberFormatException ignored) {
proxyPort = mPreferences.getProxyPort();
proxyPort = mPreferenceManager.getProxyPort();
}
mPreferences.setProxyHost(proxyHost);
mPreferences.setProxyPort(proxyPort);
mPreferenceManager.setProxyHost(proxyHost);
mPreferenceManager.setProxyPort(proxyPort);
proxy.setSummary(proxyHost + ':' + proxyPort);
}
}).show();
@@ -292,29 +280,24 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
"DuckDuckGo (Privacy)", "DuckDuckGo Lite (Privacy)", "Baidu (Chinese)",
"Yandex (Russian)"};
int n = mPreferences.getSearchChoice();
int n = mPreferenceManager.getSearchChoice();
picker.setSingleChoiceItems(chars, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setSearchChoice(which);
mPreferenceManager.setSearchChoice(which);
setSearchEngineSummary(which);
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
picker.show();
}
private void homepageDialog() {
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.home));
mHomepage = mPreferences.getHomepage();
mHomepage = mPreferenceManager.getHomepage();
int n;
if (mHomepage.contains("about:home")) {
n = 1;
@@ -332,15 +315,15 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
public void onClick(DialogInterface dialog, int which) {
switch (which + 1) {
case 1:
mPreferences.setHomepage("about:home");
mPreferenceManager.setHomepage("about:home");
home.setSummary(getResources().getString(R.string.action_homepage));
break;
case 2:
mPreferences.setHomepage("about:blank");
mPreferenceManager.setHomepage("about:blank");
home.setSummary(getResources().getString(R.string.action_blank));
break;
case 3:
mPreferences.setHomepage("about:bookmarks");
mPreferenceManager.setHomepage("about:bookmarks");
home.setSummary(getResources().getString(R.string.action_bookmarks));
break;
case 4:
@@ -349,12 +332,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
}
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
picker.show();
}
@@ -362,11 +340,12 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
final AlertDialog.Builder homePicker = new AlertDialog.Builder(mActivity);
homePicker.setTitle(getResources().getString(R.string.title_custom_homepage));
final EditText getHome = new EditText(mActivity);
mHomepage = mPreferences.getHomepage();
mHomepage = mPreferenceManager.getHomepage();
if (!mHomepage.startsWith("about:")) {
getHome.setText(mHomepage);
} else {
getHome.setText("http://www.google.com");
String defaultUrl = "https://www.google.com";
getHome.setText(defaultUrl);
}
homePicker.setView(getHome);
homePicker.setPositiveButton(getResources().getString(R.string.action_ok),
@@ -374,7 +353,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
@Override
public void onClick(DialogInterface dialog, int which) {
String text = getHome.getText().toString();
mPreferences.setHomepage(text);
mPreferenceManager.setHomepage(text);
home.setSummary(text);
}
});
@@ -384,48 +363,42 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
private void downloadLocDialog() {
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.title_download_location));
mDownloadLocation = mPreferences.getDownloadDirectory();
mDownloadLocation = mPreferenceManager.getDownloadDirectory();
int n;
if (mDownloadLocation.contains(Environment.DIRECTORY_DOWNLOADS)) {
n = 1;
n = 0;
} else {
n = 2;
n = 1;
}
picker.setSingleChoiceItems(R.array.download_folder, n - 1,
picker.setSingleChoiceItems(R.array.download_folder, n,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which + 1) {
case 1:
mPreferences.setDownloadDirectory(Environment.DIRECTORY_DOWNLOADS);
downloadloc.setSummary(Constants.EXTERNAL_STORAGE + '/'
+ Environment.DIRECTORY_DOWNLOADS);
switch (which) {
case 0:
mPreferenceManager.setDownloadDirectory(DownloadHandler.DEFAULT_DOWNLOAD_PATH);
downloadloc.setSummary(DownloadHandler.DEFAULT_DOWNLOAD_PATH);
break;
case 2:
case 1:
downPicker();
break;
}
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
picker.show();
}
private void agentDialog() {
AlertDialog.Builder agentPicker = new AlertDialog.Builder(mActivity);
agentPicker.setTitle(getResources().getString(R.string.title_user_agent));
mAgentChoice = mPreferences.getUserAgentChoice();
mAgentChoice = mPreferenceManager.getUserAgentChoice();
agentPicker.setSingleChoiceItems(R.array.user_agent, mAgentChoice - 1,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setUserAgentChoice(which + 1);
mPreferenceManager.setUserAgentChoice(which + 1);
switch (which + 1) {
case 1:
useragent.setSummary(getResources().getString(R.string.agent_default));
@@ -443,18 +416,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
}
}
});
agentPicker.setNeutralButton(getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
agentPicker.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
Log.i("Cancelled", "");
}
});
agentPicker.setNeutralButton(getResources().getString(R.string.action_ok), null);
agentPicker.show();
}
@@ -468,7 +430,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
@Override
public void onClick(DialogInterface dialog, int which) {
String text = getAgent.getText().toString();
mPreferences.setUserAgentString(text);
mPreferenceManager.setUserAgentString(text);
useragent.setSummary(getResources().getString(R.string.agent_custom));
}
});
@@ -480,36 +442,25 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
LinearLayout layout = new LinearLayout(mActivity);
downLocationPicker.setTitle(getResources().getString(R.string.title_download_location));
final EditText getDownload = new EditText(mActivity);
getDownload.setText(mPreferences.getDownloadDirectory());
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());
getDownload.setTextColor(regularColor);
getDownload.addTextChangedListener(new DownloadLocationTextWatcher(getDownload, errorColor, regularColor));
getDownload.setText(mPreferenceManager.getDownloadDirectory());
int padding = Utils.dpToPx(10);
TextView v = new TextView(mActivity);
v.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
v.setTextColor(Color.DKGRAY);
v.setText(Constants.EXTERNAL_STORAGE + '/');
v.setPadding(padding, padding, 0, padding);
layout.addView(v);
layout.addView(getDownload);
if (API < Build.VERSION_CODES.JELLY_BEAN) {
layout.setBackgroundDrawable(getResources().getDrawable(android.R.drawable.edit_text));
} else {
Drawable drawable;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
drawable = getResources().getDrawable(android.R.drawable.edit_text, getActivity().getTheme());
} else {
drawable = getResources().getDrawable(android.R.drawable.edit_text);
}
layout.setBackground(drawable);
}
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();
mPreferences.setDownloadDirectory(text);
downloadloc.setSummary(Constants.EXTERNAL_STORAGE + '/' + text);
text = DownloadHandler.addNecessarySlashes(text);
mPreferenceManager.setDownloadDirectory(text);
downloadloc.setSummary(text);
}
});
downLocationPicker.show();
@@ -553,7 +504,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
}
@Override
public boolean onPreferenceClick(Preference preference) {
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_PROXY:
proxyChoicePicker();
@@ -576,47 +527,72 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
// switch preferences
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
boolean checked = false;
if (newValue instanceof Boolean) {
checked = (Boolean) newValue;
}
switch (preference.getKey()) {
case SETTINGS_FLASH:
if (cbFlash.isChecked()) {
getFlashChoice();
} else {
mPreferences.setFlashSupport(0);
}
if (!Utils.isFlashInstalled(mActivity) && cbFlash.isChecked()) {
if (!Utils.isFlashInstalled(mActivity) && checked) {
Utils.createInformativeDialog(mActivity, R.string.title_warning, R.string.dialog_adobe_not_installed);
cbFlash.setEnabled(false);
mPreferences.setFlashSupport(0);
mPreferenceManager.setFlashSupport(0);
return false;
} else {
if (checked) {
getFlashChoice();
} else {
mPreferenceManager.setFlashSupport(0);
}
}
cbFlash.setChecked((Boolean) newValue);
return true;
case SETTINGS_ADS:
mPreferences.setAdBlockEnabled((Boolean) newValue);
cbAds.setChecked((Boolean) newValue);
mPreferenceManager.setAdBlockEnabled(checked);
return true;
case SETTINGS_IMAGES:
mPreferences.setBlockImagesEnabled((Boolean) newValue);
cbImages.setChecked((Boolean) newValue);
mPreferenceManager.setBlockImagesEnabled(checked);
return true;
case SETTINGS_JAVASCRIPT:
mPreferences.setJavaScriptEnabled((Boolean) newValue);
cbJsScript.setChecked((Boolean) newValue);
mPreferenceManager.setJavaScriptEnabled(checked);
return true;
case SETTINGS_COLORMODE:
mPreferences.setColorModeEnabled((Boolean) newValue);
cbColorMode.setChecked((Boolean) newValue);
mPreferenceManager.setColorModeEnabled(checked);
return true;
case SETTINGS_GOOGLESUGGESTIONS:
mPreferences.setGoogleSearchSuggestionsEnabled((Boolean) newValue);
cbgooglesuggest.setChecked((Boolean) newValue);
mPreferenceManager.setGoogleSearchSuggestionsEnabled(checked);
return true;
case SETTINGS_DRAWERTABS:
mPreferenceManager.setShowTabsInDrawer(checked);
return true;
case SETTINGS_DRAWERTABS:
mPreferences.setShowTabsInDrawer((Boolean) newValue);
cbDrawerTabs.setChecked((Boolean) newValue);
default:
return false;
}
}
private static class DownloadLocationTextWatcher implements TextWatcher {
private final EditText getDownload;
private final int errorColor;
private final int regularColor;
public DownloadLocationTextWatcher(EditText getDownload, int errorColor, int regularColor) {
this.getDownload = getDownload;
this.errorColor = errorColor;
this.regularColor = regularColor;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(@NonNull Editable s) {
if (!DownloadHandler.isWriteAccessAvailable(s.toString())) {
this.getDownload.setTextColor(this.errorColor);
} else {
this.getDownload.setTextColor(this.regularColor);
}
}
}
}
@@ -0,0 +1,27 @@
package acr.browser.lightning.fragment;
import android.os.Bundle;
import android.preference.PreferenceFragment;
import javax.inject.Inject;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.preference.PreferenceManager;
/**
* Simplify {@link PreferenceManager} inject in all the PreferenceFragments
*
* @author Stefano Pacifici
* @date 2015/09/16
*/
public class LightningPreferenceFragment extends PreferenceFragment {
@Inject
PreferenceManager mPreferenceManager;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
BrowserApp.getAppComponent().inject(this);
}
}
@@ -11,16 +11,20 @@ import android.os.Handler;
import android.os.Message;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.webkit.WebView;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.database.HistoryDatabase;
import acr.browser.lightning.utils.Utils;
import acr.browser.lightning.utils.WebUtils;
import acr.browser.lightning.view.LightningView;
public class PrivacySettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
public class PrivacySettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
private static final String SETTINGS_LOCATION = "location";
private static final String SETTINGS_THIRDPCOOKIES = "third_party";
@@ -33,16 +37,18 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
private static final String SETTINGS_CLEARCOOKIES = "clear_cookies";
private static final String SETTINGS_CLEARWEBSTORAGE = "clear_webstorage";
private static final String SETTINGS_WEBSTORAGEEXIT = "clear_webstorage_exit";
private static final String SETTINGS_DONOTTRACK = "do_not_track";
private static final String SETTINGS_IDENTIFYINGHEADERS = "remove_identifying_headers";
private Activity mActivity;
private PreferenceManager mPreferences;
private CheckBoxPreference cblocation, cb3cookies, cbsavepasswords, cbcacheexit, cbhistoryexit,
cbcookiesexit, cbwebstorageexit;
private Handler messageHandler;
private Handler mMessageHandler;
@Inject HistoryDatabase mHistoryDatabase;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
BrowserApp.getAppComponent().inject(this);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preference_privacy);
@@ -52,21 +58,20 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
}
private void initPrefs() {
// mPreferences storage
mPreferences = PreferenceManager.getInstance();
Preference clearcache = findPreference(SETTINGS_CLEARCACHE);
Preference clearhistory = findPreference(SETTINGS_CLEARHISTORY);
Preference clearcookies = findPreference(SETTINGS_CLEARCOOKIES);
Preference clearwebstorage = findPreference(SETTINGS_CLEARWEBSTORAGE);
cblocation = (CheckBoxPreference) findPreference(SETTINGS_LOCATION);
cb3cookies = (CheckBoxPreference) findPreference(SETTINGS_THIRDPCOOKIES);
cbsavepasswords = (CheckBoxPreference) findPreference(SETTINGS_SAVEPASSWORD);
cbcacheexit = (CheckBoxPreference) findPreference(SETTINGS_CACHEEXIT);
cbhistoryexit = (CheckBoxPreference) findPreference(SETTINGS_HISTORYEXIT);
cbcookiesexit = (CheckBoxPreference) findPreference(SETTINGS_COOKIEEXIT);
cbwebstorageexit = (CheckBoxPreference) findPreference(SETTINGS_WEBSTORAGEEXIT);
CheckBoxPreference cblocation = (CheckBoxPreference) findPreference(SETTINGS_LOCATION);
CheckBoxPreference cb3cookies = (CheckBoxPreference) findPreference(SETTINGS_THIRDPCOOKIES);
CheckBoxPreference cbsavepasswords = (CheckBoxPreference) findPreference(SETTINGS_SAVEPASSWORD);
CheckBoxPreference cbcacheexit = (CheckBoxPreference) findPreference(SETTINGS_CACHEEXIT);
CheckBoxPreference cbhistoryexit = (CheckBoxPreference) findPreference(SETTINGS_HISTORYEXIT);
CheckBoxPreference cbcookiesexit = (CheckBoxPreference) findPreference(SETTINGS_COOKIEEXIT);
CheckBoxPreference cbwebstorageexit = (CheckBoxPreference) findPreference(SETTINGS_WEBSTORAGEEXIT);
CheckBoxPreference cbDoNotTrack = (CheckBoxPreference) findPreference(SETTINGS_DONOTTRACK);
CheckBoxPreference cbIdentifyingHeaders = (CheckBoxPreference) findPreference(SETTINGS_IDENTIFYINGHEADERS);
clearcache.setOnPreferenceClickListener(this);
clearhistory.setOnPreferenceClickListener(this);
@@ -80,18 +85,28 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
cbhistoryexit.setOnPreferenceChangeListener(this);
cbcookiesexit.setOnPreferenceChangeListener(this);
cbwebstorageexit.setOnPreferenceChangeListener(this);
cbDoNotTrack.setOnPreferenceChangeListener(this);
cbIdentifyingHeaders.setOnPreferenceChangeListener(this);
cblocation.setChecked(mPreferences.getLocationEnabled());
cbsavepasswords.setChecked(mPreferences.getSavePasswordsEnabled());
cbcacheexit.setChecked(mPreferences.getClearCacheExit());
cbhistoryexit.setChecked(mPreferences.getClearHistoryExitEnabled());
cbcookiesexit.setChecked(mPreferences.getClearCookiesExitEnabled());
cb3cookies.setChecked(mPreferences.getBlockThirdPartyCookiesEnabled());
cbwebstorageexit.setChecked(mPreferences.getClearWebStorageExitEnabled());
cblocation.setChecked(mPreferenceManager.getLocationEnabled());
cbsavepasswords.setChecked(mPreferenceManager.getSavePasswordsEnabled());
cbcacheexit.setChecked(mPreferenceManager.getClearCacheExit());
cbhistoryexit.setChecked(mPreferenceManager.getClearHistoryExitEnabled());
cbcookiesexit.setChecked(mPreferenceManager.getClearCookiesExitEnabled());
cb3cookies.setChecked(mPreferenceManager.getBlockThirdPartyCookiesEnabled());
cbwebstorageexit.setChecked(mPreferenceManager.getClearWebStorageExitEnabled());
cbDoNotTrack.setChecked(mPreferenceManager.getDoNotTrackEnabled() && Utils.doesSupportHeaders());
cbIdentifyingHeaders.setChecked(mPreferenceManager.getRemoveIdentifyingHeadersEnabled() && Utils.doesSupportHeaders());
cbDoNotTrack.setEnabled(Utils.doesSupportHeaders());
cbIdentifyingHeaders.setEnabled(Utils.doesSupportHeaders());
String identifyingHeadersSummary = LightningView.HEADER_REQUESTED_WITH + ", " + LightningView.HEADER_WAP_PROFILE;
cbIdentifyingHeaders.setSummary(identifyingHeadersSummary);
cb3cookies.setEnabled(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP);
messageHandler = new MessageHandler(mActivity);
mMessageHandler = new MessageHandler(mActivity);
}
private static class MessageHandler extends Handler {
@@ -103,7 +118,7 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
}
@Override
public void handleMessage(Message msg) {
public void handleMessage(@NonNull Message msg) {
switch (msg.what) {
case 1:
Utils.showSnackbar(mHandlerContext, R.string.message_clear_history);
@@ -117,7 +132,7 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
}
@Override
public boolean onPreferenceClick(Preference preference) {
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_CLEARCACHE:
clearCache();
@@ -144,21 +159,15 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
Thread clear = new Thread(new Runnable() {
BrowserApp.getIOThread().execute(new Runnable() {
@Override
public void run() {
clearHistory();
}
});
clear.start();
}
})
.setNegativeButton(getResources().getString(R.string.action_no),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
}
}).show();
.setNegativeButton(getResources().getString(R.string.action_no), null).show();
}
private void clearCookiesDialog() {
@@ -169,21 +178,15 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
Thread clear = new Thread(new Runnable() {
BrowserApp.getTaskThread().execute(new Runnable() {
@Override
public void run() {
clearCookies();
}
});
clear.start();
}
})
.setNegativeButton(getResources().getString(R.string.action_no),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
}
}).show();
.setNegativeButton(getResources().getString(R.string.action_no), null).show();
}
private void clearCache() {
@@ -194,13 +197,13 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
}
private void clearHistory() {
WebUtils.clearHistory(getActivity());
messageHandler.sendEmptyMessage(1);
WebUtils.clearHistory(getActivity(), mHistoryDatabase);
mMessageHandler.sendEmptyMessage(1);
}
private void clearCookies() {
WebUtils.clearCookies(getActivity());
messageHandler.sendEmptyMessage(2);
mMessageHandler.sendEmptyMessage(2);
}
private void clearWebStorage() {
@@ -209,36 +212,34 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
// switch preferences
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
switch (preference.getKey()) {
case SETTINGS_LOCATION:
mPreferences.setLocationEnabled((Boolean) newValue);
cblocation.setChecked((Boolean) newValue);
mPreferenceManager.setLocationEnabled((Boolean) newValue);
return true;
case SETTINGS_THIRDPCOOKIES:
mPreferences.setBlockThirdPartyCookiesEnabled((Boolean) newValue);
cb3cookies.setChecked((Boolean) newValue);
mPreferenceManager.setBlockThirdPartyCookiesEnabled((Boolean) newValue);
return true;
case SETTINGS_SAVEPASSWORD:
mPreferences.setSavePasswordsEnabled((Boolean) newValue);
cbsavepasswords.setChecked((Boolean) newValue);
mPreferenceManager.setSavePasswordsEnabled((Boolean) newValue);
return true;
case SETTINGS_CACHEEXIT:
mPreferences.setClearCacheExit((Boolean) newValue);
cbcacheexit.setChecked((Boolean) newValue);
mPreferenceManager.setClearCacheExit((Boolean) newValue);
return true;
case SETTINGS_HISTORYEXIT:
mPreferences.setClearHistoryExitEnabled((Boolean) newValue);
cbhistoryexit.setChecked((Boolean) newValue);
mPreferenceManager.setClearHistoryExitEnabled((Boolean) newValue);
return true;
case SETTINGS_COOKIEEXIT:
mPreferences.setClearCookiesExitEnabled((Boolean) newValue);
cbcookiesexit.setChecked((Boolean) newValue);
mPreferenceManager.setClearCookiesExitEnabled((Boolean) newValue);
return true;
case SETTINGS_WEBSTORAGEEXIT:
mPreferences.setClearWebStorageExitEnabled((Boolean) newValue);
cbwebstorageexit.setChecked((Boolean) newValue);
mPreferenceManager.setClearWebStorageExitEnabled((Boolean) newValue);
return true;
case SETTINGS_DONOTTRACK:
mPreferenceManager.setDoNotTrackEnabled((Boolean) newValue);
return true;
case SETTINGS_IDENTIFYINGHEADERS:
mPreferenceManager.setRemoveIdentifyingHeadersEnabled((Boolean) newValue);
return true;
default:
return false;
@@ -0,0 +1,413 @@
package acr.browser.lightning.fragment;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.TextViewCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.LayoutManager;
import android.support.v7.widget.SimpleItemAnimator;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.squareup.otto.Bus;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.activity.TabsManager;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.browser.TabsView;
import acr.browser.lightning.bus.NavigationEvents;
import acr.browser.lightning.bus.TabEvents;
import acr.browser.lightning.controller.UIController;
import acr.browser.lightning.fragment.anim.HorizontalItemAnimator;
import acr.browser.lightning.fragment.anim.VerticalItemAnimator;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.ThemeUtils;
import acr.browser.lightning.utils.Utils;
import acr.browser.lightning.view.LightningView;
/**
* A fragment that holds and manages the tabs and interaction with the tabs.
* It is reliant on the BrowserController in order to get the current UI state
* of the browser. It also uses the BrowserController to signal that the UI needs
* to change. This class contains the adapter used by both the drawer tabs and
* the desktop tabs. It delegates touch events for the tab UI appropriately.
*/
public class TabsFragment extends Fragment implements View.OnClickListener, View.OnLongClickListener, TabsView {
private static final String TAG = TabsFragment.class.getSimpleName();
/**
* 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 boolean mIsIncognito, mDarkTheme;
private int mIconColor;
private boolean mColorMode = true;
private boolean mShowInNavigationDrawer;
@Nullable private LightningViewAdapter mTabsAdapter;
private UIController mUiController;
private RecyclerView mRecyclerView;
private TabsManager mTabsManager;
@Inject Bus mBus;
@Inject PreferenceManager mPreferences;
public TabsFragment() {
BrowserApp.getAppComponent().inject(this);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Bundle arguments = getArguments();
final Context context = getContext();
mUiController = (UIController) getActivity();
mTabsManager = mUiController.getTabModel();
mIsIncognito = arguments.getBoolean(IS_INCOGNITO, false);
mShowInNavigationDrawer = arguments.getBoolean(VERTICAL_MODE, true);
mDarkTheme = mPreferences.getUseTheme() != 0 || mIsIncognito;
mColorMode = mPreferences.getColorModeEnabled();
mColorMode &= !mDarkTheme;
mIconColor = mDarkTheme ?
ThemeUtils.getIconDarkThemeColor(context) :
ThemeUtils.getIconLightThemeColor(context);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view;
final LayoutManager layoutManager;
if (mShowInNavigationDrawer) {
view = inflater.inflate(R.layout.tab_drawer, container, false);
layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
setupFrameLayoutButton(view, R.id.tab_header_button, R.id.plusIcon);
setupFrameLayoutButton(view, R.id.new_tab_button, R.id.icon_plus);
setupFrameLayoutButton(view, R.id.action_back, R.id.icon_back);
setupFrameLayoutButton(view, R.id.action_forward, R.id.icon_forward);
setupFrameLayoutButton(view, R.id.action_home, R.id.icon_home);
} else {
view = inflater.inflate(R.layout.tab_strip, container, false);
layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false);
ImageView newTab = (ImageView) view.findViewById(R.id.new_tab_button);
newTab.setColorFilter(ThemeUtils.getIconDarkThemeColor(getActivity()));
newTab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mUiController.newTabClicked();
}
});
}
mRecyclerView = (RecyclerView) view.findViewById(R.id.tabs_list);
SimpleItemAnimator animator;
if (mShowInNavigationDrawer) {
animator = new VerticalItemAnimator();
} else {
animator = new HorizontalItemAnimator();
}
animator.setSupportsChangeAnimations(false);
animator.setAddDuration(200);
animator.setChangeDuration(0);
animator.setRemoveDuration(200);
animator.setMoveDuration(200);
mRecyclerView.setLayerType(View.LAYER_TYPE_NONE, null);
mRecyclerView.setItemAnimator(animator);
mRecyclerView.setLayoutManager(layoutManager);
mTabsAdapter = new LightningViewAdapter(mShowInNavigationDrawer);
mRecyclerView.setAdapter(mTabsAdapter);
mRecyclerView.setHasFixedSize(true);
return view;
}
private void setupFrameLayoutButton(@NonNull final View root, @IdRes final int buttonId,
@IdRes final int imageId) {
final View frameButton = root.findViewById(buttonId);
final ImageView buttonImage = (ImageView) root.findViewById(imageId);
frameButton.setOnClickListener(this);
frameButton.setOnLongClickListener(this);
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();
// Force adapter refresh
if (mTabsAdapter != null) {
mTabsAdapter.notifyDataSetChanged();
}
}
@Override
public void onStop() {
super.onStop();
mBus.unregister(this);
}
public void reinitializePreferences() {
Activity activity = getActivity();
if (activity == null) {
return;
}
mDarkTheme = mPreferences.getUseTheme() != 0 || mIsIncognito;
mColorMode = mPreferences.getColorModeEnabled();
mColorMode &= !mDarkTheme;
mIconColor = mDarkTheme ?
ThemeUtils.getIconDarkThemeColor(activity) :
ThemeUtils.getIconLightThemeColor(activity);
if (mTabsAdapter != null) {
mTabsAdapter.notifyDataSetChanged();
}
}
@Override
public void onClick(@NonNull View v) {
switch (v.getId()) {
case R.id.tab_header_button:
mUiController.showCloseDialog(mTabsManager.indexOfCurrentTab());
break;
case R.id.new_tab_button:
mBus.post(new TabEvents.NewTab());
break;
case R.id.action_back:
mBus.post(new NavigationEvents.GoBack());
break;
case R.id.action_forward:
mBus.post(new NavigationEvents.GoForward());
break;
case R.id.action_home:
mBus.post(new NavigationEvents.GoHome());
default:
break;
}
}
@Override
public boolean onLongClick(@NonNull View v) {
switch (v.getId()) {
case R.id.action_new_tab:
mBus.post(new TabEvents.NewTabLongPress());
break;
default:
break;
}
return true;
}
@Override
public void tabAdded() {
if (mTabsAdapter != null) {
mTabsAdapter.notifyItemInserted(mTabsManager.last());
mRecyclerView.postDelayed(new Runnable() {
@Override
public void run() {
mRecyclerView.smoothScrollToPosition(mTabsAdapter.getItemCount() - 1);
}
}, 500);
}
}
@Override
public void tabRemoved(int position) {
if (mTabsAdapter != null) {
mTabsAdapter.notifyItemRemoved(position);
}
}
@Override
public void tabChanged(int position) {
if (mTabsAdapter != null) {
mTabsAdapter.notifyItemChanged(position);
}
}
private class LightningViewAdapter extends RecyclerView.Adapter<LightningViewAdapter.LightningViewHolder> {
private final int mLayoutResourceId;
@Nullable private final Drawable mBackgroundTabDrawable;
@Nullable private final Drawable mForegroundTabDrawable;
@Nullable private final Bitmap mForegroundTabBitmap;
private ColorMatrix mColorMatrix;
private Paint mPaint;
private ColorFilter mFilter;
private static final float DESATURATED = 0.5f;
private final boolean mDrawerTabs;
public LightningViewAdapter(final boolean vertical) {
this.mLayoutResourceId = vertical ? R.layout.tab_list_item : R.layout.tab_list_item_horizontal;
this.mDrawerTabs = vertical;
if (vertical) {
mBackgroundTabDrawable = null;
mForegroundTabBitmap = null;
mForegroundTabDrawable = ThemeUtils.getSelectedBackground(getContext(), mDarkTheme);
} else {
int backgroundColor = Utils.mixTwoColors(ThemeUtils.getPrimaryColor(getContext()), Color.BLACK, 0.75f);
Bitmap backgroundTabBitmap = Bitmap.createBitmap(Utils.dpToPx(175), Utils.dpToPx(30), Bitmap.Config.ARGB_8888);
Utils.drawTrapezoid(new Canvas(backgroundTabBitmap), backgroundColor, true);
mBackgroundTabDrawable = new BitmapDrawable(getResources(), backgroundTabBitmap);
int foregroundColor = ThemeUtils.getPrimaryColor(getContext());
mForegroundTabBitmap = Bitmap.createBitmap(Utils.dpToPx(175), Utils.dpToPx(30), Bitmap.Config.ARGB_8888);
Utils.drawTrapezoid(new Canvas(mForegroundTabBitmap), foregroundColor, false);
mForegroundTabDrawable = null;
}
}
@NonNull
@Override
public LightningViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
View view = inflater.inflate(mLayoutResourceId, viewGroup, false);
return new LightningViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final LightningViewHolder holder, int position) {
holder.exitButton.setTag(position);
ViewCompat.jumpDrawablesToCurrentState(holder.exitButton);
LightningView web = mTabsManager.getTabAtPosition(position);
if (web == null) {
return;
}
holder.txtTitle.setText(web.getTitle());
final Bitmap favicon = web.getFavicon();
if (web.isForegroundTab()) {
TextViewCompat.setTextAppearance(holder.txtTitle, R.style.boldText);
Drawable foregroundDrawable;
if (!mDrawerTabs) {
foregroundDrawable = new BitmapDrawable(getResources(), mForegroundTabBitmap);
if (!mIsIncognito && mColorMode) {
foregroundDrawable.setColorFilter(mUiController.getUiColor(), PorterDuff.Mode.SRC_IN);
}
} else {
foregroundDrawable = mForegroundTabDrawable;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
holder.layout.setBackground(foregroundDrawable);
} else {
holder.layout.setBackgroundDrawable(foregroundDrawable);
}
if (!mIsIncognito && mColorMode) {
mUiController.changeToolbarBackground(favicon, foregroundDrawable);
}
holder.favicon.setImageBitmap(favicon);
} else {
TextViewCompat.setTextAppearance(holder.txtTitle, R.style.normalText);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
holder.layout.setBackground(mBackgroundTabDrawable);
} else {
holder.layout.setBackgroundDrawable(mBackgroundTabDrawable);
}
holder.favicon.setImageBitmap(getDesaturatedBitmap(favicon));
}
}
@Override
public int getItemCount() {
return mTabsManager.size();
}
public Bitmap getDesaturatedBitmap(@NonNull Bitmap favicon) {
Bitmap grayscaleBitmap = Bitmap.createBitmap(favicon.getWidth(),
favicon.getHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(grayscaleBitmap);
if (mColorMatrix == null || mFilter == null || mPaint == null) {
mPaint = new Paint();
mColorMatrix = new ColorMatrix();
mColorMatrix.setSaturation(DESATURATED);
mFilter = new ColorMatrixColorFilter(mColorMatrix);
mPaint.setColorFilter(mFilter);
}
c.drawBitmap(favicon, 0, 0, mPaint);
return grayscaleBitmap;
}
public class LightningViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
public LightningViewHolder(@NonNull View view) {
super(view);
txtTitle = (TextView) view.findViewById(R.id.textTab);
favicon = (ImageView) view.findViewById(R.id.faviconTab);
exit = (ImageView) view.findViewById(R.id.deleteButton);
layout = (LinearLayout) view.findViewById(R.id.tab_item_background);
exitButton = (FrameLayout) view.findViewById(R.id.deleteAction);
exit.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN);
exitButton.setOnClickListener(this);
layout.setOnClickListener(this);
layout.setOnLongClickListener(this);
}
@NonNull final TextView txtTitle;
@NonNull final ImageView favicon;
@NonNull final ImageView exit;
@NonNull final FrameLayout exitButton;
@NonNull final LinearLayout layout;
@Override
public void onClick(View v) {
if (v == exitButton) {
// Close tab
mBus.post(new TabEvents.CloseTab(getAdapterPosition()));
}
if (v == layout) {
mBus.post(new TabEvents.ShowTab(getAdapterPosition()));
}
}
@Override
public boolean onLongClick(View v) {
// Show close dialog
mBus.post(new TabEvents.ShowCloseDialog(getAdapterPosition()));
return true;
}
}
}
}
@@ -0,0 +1,675 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package acr.browser.lightning.fragment.anim;
import android.support.v4.animation.AnimatorCompatHelper;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPropertyAnimatorCompat;
import android.support.v4.view.ViewPropertyAnimatorListener;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.SimpleItemAnimator;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import java.util.ArrayList;
import java.util.List;
/**
* This implementation of {@link RecyclerView.ItemAnimator} provides basic
* animations on remove, add, and move events that happen to the items in
* a RecyclerView. RecyclerView uses a HorizontalItemAnimator by default.
*
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
*/
public class HorizontalItemAnimator extends SimpleItemAnimator {
private static final boolean DEBUG = false;
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
private ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
private ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
private ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
private static class MoveInfo {
public ViewHolder holder;
public int fromX, fromY, toX, toY;
private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
this.holder = holder;
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
}
private static class ChangeInfo {
public ViewHolder oldHolder, newHolder;
public int fromX, fromY, toX, toY;
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
this.oldHolder = oldHolder;
this.newHolder = newHolder;
}
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
this(oldHolder, newHolder);
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
@Override
public String toString() {
return "ChangeInfo{" +
"oldHolder=" + oldHolder +
", newHolder=" + newHolder +
", fromX=" + fromX +
", fromY=" + fromY +
", toX=" + toX +
", toY=" + toY +
'}';
}
}
@Override
public void runPendingAnimations() {
boolean removalsPending = !mPendingRemovals.isEmpty();
boolean movesPending = !mPendingMoves.isEmpty();
boolean changesPending = !mPendingChanges.isEmpty();
boolean additionsPending = !mPendingAdditions.isEmpty();
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
// nothing to animate
return;
}
// First, remove stuff
for (ViewHolder holder : mPendingRemovals) {
animateRemoveImpl(holder);
}
mPendingRemovals.clear();
// Next, move stuff
if (movesPending) {
final ArrayList<MoveInfo> moves = new ArrayList<>();
moves.addAll(mPendingMoves);
mMovesList.add(moves);
mPendingMoves.clear();
Runnable mover = new Runnable() {
@Override
public void run() {
for (MoveInfo moveInfo : moves) {
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
moveInfo.toX, moveInfo.toY);
}
moves.clear();
mMovesList.remove(moves);
}
};
if (removalsPending) {
View view = moves.get(0).holder.itemView;
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
} else {
mover.run();
}
}
// Next, change stuff, to run in parallel with move animations
if (changesPending) {
final ArrayList<ChangeInfo> changes = new ArrayList<>();
changes.addAll(mPendingChanges);
mChangesList.add(changes);
mPendingChanges.clear();
Runnable changer = new Runnable() {
@Override
public void run() {
for (ChangeInfo change : changes) {
animateChangeImpl(change);
}
changes.clear();
mChangesList.remove(changes);
}
};
if (removalsPending) {
ViewHolder holder = changes.get(0).oldHolder;
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
} else {
changer.run();
}
}
// Next, add stuff
if (additionsPending) {
final ArrayList<ViewHolder> additions = new ArrayList<>();
additions.addAll(mPendingAdditions);
mAdditionsList.add(additions);
mPendingAdditions.clear();
Runnable adder = new Runnable() {
public void run() {
for (ViewHolder holder : additions) {
animateAddImpl(holder);
}
additions.clear();
mAdditionsList.remove(additions);
}
};
if (removalsPending || movesPending || changesPending) {
long removeDuration = removalsPending ? getRemoveDuration() : 0;
long moveDuration = movesPending ? getMoveDuration() : 0;
long changeDuration = changesPending ? getChangeDuration() : 0;
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
View view = additions.get(0).itemView;
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
} else {
adder.run();
}
}
}
@Override
public boolean animateRemove(final ViewHolder holder) {
resetAnimation(holder);
mPendingRemovals.add(holder);
return true;
}
private void animateRemoveImpl(final ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mRemoveAnimations.add(holder);
animation.setDuration(getRemoveDuration())
.alpha(0).translationY(holder.itemView.getHeight())
.setInterpolator(new AccelerateInterpolator()).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchRemoveStarting(holder);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationY(view, 0);
dispatchRemoveFinished(holder);
mRemoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateAdd(final ViewHolder holder) {
resetAnimation(holder);
ViewCompat.setAlpha(holder.itemView, 0);
ViewCompat.setTranslationY(holder.itemView, holder.itemView.getHeight());
mPendingAdditions.add(holder);
return true;
}
private void animateAddImpl(final ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mAddAnimations.add(holder);
animation.alpha(1).translationY(0)
.setInterpolator(new DecelerateInterpolator()).setDuration(getAddDuration())
.setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchAddStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setAlpha(view, 1);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchAddFinished(holder);
mAddAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
int toX, int toY) {
final View view = holder.itemView;
fromX += ViewCompat.getTranslationX(holder.itemView);
fromY += ViewCompat.getTranslationY(holder.itemView);
int deltaX = toX - fromX;
int deltaY = toY - fromY;
if (deltaX == 0 && deltaY == 0) {
dispatchMoveFinished(holder);
return false;
}
resetAnimation(holder);
if (deltaX != 0) {
ViewCompat.setTranslationX(view, -deltaX);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, -deltaY);
}
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
return true;
}
private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
final View view = holder.itemView;
final int deltaX = toX - fromX;
final int deltaY = toY - fromY;
if (deltaX != 0) {
ViewCompat.animate(view).translationX(0);
}
if (deltaY != 0) {
ViewCompat.animate(view).translationY(0);
}
// TODO: make EndActions end listeners instead, since end actions aren't called when
// vpas are canceled (and can't end them. why?)
// need listener functionality in VPACompat for this. Ick.
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mMoveAnimations.add(holder);
animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchMoveStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
if (deltaX != 0) {
ViewCompat.setTranslationX(view, 0);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, 0);
}
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchMoveFinished(holder);
mMoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
// if (oldHolder != newHolder) {
// if (oldHolder != null) {
// dispatchChangeFinished(oldHolder, true);
// }
// if (newHolder != null) {
// dispatchChangeFinished(newHolder, false);
// }
// } else if (oldHolder != null) {
// dispatchChangeFinished(oldHolder, true);
// }
// return false;
if (oldHolder == newHolder) {
// Don't know how to run change animations when the same view holder is re-used.
// run a move animation to handle position changes.
if ((fromX - toX) == 0 && (fromY - toY) == 0) {
dispatchMoveFinished(oldHolder);
return false;
}
return animateMove(oldHolder, fromX, fromY, toX, toY);
}
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
resetAnimation(oldHolder);
int deltaX = (int) (toX - fromX - prevTranslationX);
int deltaY = (int) (toY - fromY - prevTranslationY);
// recover prev translation state after ending animation
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
if (newHolder != null) {
// carry over translation values
resetAnimation(newHolder);
ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
ViewCompat.setAlpha(newHolder.itemView, 0);
}
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
return true;
}
private void animateChangeImpl(final ChangeInfo changeInfo) {
final ViewHolder holder = changeInfo.oldHolder;
final View view = holder == null ? null : holder.itemView;
final ViewHolder newHolder = changeInfo.newHolder;
final View newView = newHolder != null ? newHolder.itemView : null;
if (view != null) {
final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
getChangeDuration());
mChangeAnimations.add(changeInfo.oldHolder);
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.oldHolder, true);
}
@Override
public void onAnimationEnd(View view) {
oldViewAnim.setListener(null);
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationX(view, 0);
ViewCompat.setTranslationY(view, 0);
dispatchChangeFinished(changeInfo.oldHolder, true);
mChangeAnimations.remove(changeInfo.oldHolder);
dispatchFinishedWhenDone();
}
}).start();
}
if (newView != null) {
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
mChangeAnimations.add(changeInfo.newHolder);
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
alpha(1).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.newHolder, false);
}
@Override
public void onAnimationEnd(View view) {
newViewAnimation.setListener(null);
ViewCompat.setAlpha(newView, 1);
ViewCompat.setTranslationX(newView, 0);
ViewCompat.setTranslationY(newView, 0);
dispatchChangeFinished(changeInfo.newHolder, false);
mChangeAnimations.remove(changeInfo.newHolder);
dispatchFinishedWhenDone();
}
}).start();
}
}
private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
for (int i = infoList.size() - 1; i >= 0; i--) {
ChangeInfo changeInfo = infoList.get(i);
if (endChangeAnimationIfNecessary(changeInfo, item)) {
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
infoList.remove(changeInfo);
}
}
}
}
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
if (changeInfo.oldHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
}
if (changeInfo.newHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
}
}
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
boolean oldItem = false;
if (changeInfo.newHolder == item) {
changeInfo.newHolder = null;
} else if (changeInfo.oldHolder == item) {
changeInfo.oldHolder = null;
oldItem = true;
} else {
return false;
}
ViewCompat.setAlpha(item.itemView, 1);
ViewCompat.setTranslationX(item.itemView, 0);
ViewCompat.setTranslationY(item.itemView, 0);
dispatchChangeFinished(item, oldItem);
return true;
}
@Override
public void endAnimation(ViewHolder item) {
final View view = item.itemView;
// this will trigger end callback which should set properties to their target values.
ViewCompat.animate(view).cancel();
// TODO if some other animations are chained to end, how do we cancel them as well?
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
MoveInfo moveInfo = mPendingMoves.get(i);
if (moveInfo.holder == item) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item);
mPendingMoves.remove(i);
}
}
endChangeAnimation(mPendingChanges, item);
if (mPendingRemovals.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchRemoveFinished(item);
}
if (mPendingAdditions.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
}
for (int i = mChangesList.size() - 1; i >= 0; i--) {
ArrayList<ChangeInfo> changes = mChangesList.get(i);
endChangeAnimation(changes, item);
if (changes.isEmpty()) {
mChangesList.remove(i);
}
}
for (int i = mMovesList.size() - 1; i >= 0; i--) {
ArrayList<MoveInfo> moves = mMovesList.get(i);
for (int j = moves.size() - 1; j >= 0; j--) {
MoveInfo moveInfo = moves.get(j);
if (moveInfo.holder == item) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item);
moves.remove(j);
if (moves.isEmpty()) {
mMovesList.remove(i);
}
break;
}
}
}
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
if (additions.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
if (additions.isEmpty()) {
mAdditionsList.remove(i);
}
}
}
// animations should be ended by the cancel above.
//noinspection PointlessBooleanExpression,ConstantConditions
if (mRemoveAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mRemoveAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mAddAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mAddAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mChangeAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mChangeAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mMoveAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mMoveAnimations list");
}
dispatchFinishedWhenDone();
}
private void resetAnimation(ViewHolder holder) {
AnimatorCompatHelper.clearInterpolator(holder.itemView);
endAnimation(holder);
}
@Override
public boolean isRunning() {
return (!mPendingAdditions.isEmpty() ||
!mPendingChanges.isEmpty() ||
!mPendingMoves.isEmpty() ||
!mPendingRemovals.isEmpty() ||
!mMoveAnimations.isEmpty() ||
!mRemoveAnimations.isEmpty() ||
!mAddAnimations.isEmpty() ||
!mChangeAnimations.isEmpty() ||
!mMovesList.isEmpty() ||
!mAdditionsList.isEmpty() ||
!mChangesList.isEmpty());
}
/**
* Check the state of currently pending and running animations. If there are none
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
* listeners.
*/
private void dispatchFinishedWhenDone() {
if (!isRunning()) {
dispatchAnimationsFinished();
}
}
@Override
public void endAnimations() {
int count = mPendingMoves.size();
for (int i = count - 1; i >= 0; i--) {
MoveInfo item = mPendingMoves.get(i);
View view = item.holder.itemView;
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item.holder);
mPendingMoves.remove(i);
}
count = mPendingRemovals.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mPendingRemovals.get(i);
dispatchRemoveFinished(item);
mPendingRemovals.remove(i);
}
count = mPendingAdditions.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mPendingAdditions.get(i);
View view = item.itemView;
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
mPendingAdditions.remove(i);
}
count = mPendingChanges.size();
for (int i = count - 1; i >= 0; i--) {
endChangeAnimationIfNecessary(mPendingChanges.get(i));
}
mPendingChanges.clear();
if (!isRunning()) {
return;
}
int listCount = mMovesList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<MoveInfo> moves = mMovesList.get(i);
count = moves.size();
for (int j = count - 1; j >= 0; j--) {
MoveInfo moveInfo = moves.get(j);
ViewHolder item = moveInfo.holder;
View view = item.itemView;
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(moveInfo.holder);
moves.remove(j);
if (moves.isEmpty()) {
mMovesList.remove(moves);
}
}
}
listCount = mAdditionsList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
count = additions.size();
for (int j = count - 1; j >= 0; j--) {
ViewHolder item = additions.get(j);
View view = item.itemView;
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
additions.remove(j);
if (additions.isEmpty()) {
mAdditionsList.remove(additions);
}
}
}
listCount = mChangesList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<ChangeInfo> changes = mChangesList.get(i);
count = changes.size();
for (int j = count - 1; j >= 0; j--) {
endChangeAnimationIfNecessary(changes.get(j));
if (changes.isEmpty()) {
mChangesList.remove(changes);
}
}
}
cancelAll(mRemoveAnimations);
cancelAll(mMoveAnimations);
cancelAll(mAddAnimations);
cancelAll(mChangeAnimations);
dispatchAnimationsFinished();
}
static void cancelAll(List<ViewHolder> viewHolders) {
for (int i = viewHolders.size() - 1; i >= 0; i--) {
ViewCompat.animate(viewHolders.get(i).itemView).cancel();
}
}
private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
@Override
public void onAnimationStart(View view) {}
@Override
public void onAnimationEnd(View view) {}
@Override
public void onAnimationCancel(View view) {}
}
}
@@ -0,0 +1,674 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package acr.browser.lightning.fragment.anim;
import android.support.v4.animation.AnimatorCompatHelper;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPropertyAnimatorCompat;
import android.support.v4.view.ViewPropertyAnimatorListener;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.SimpleItemAnimator;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import java.util.ArrayList;
import java.util.List;
/**
* This implementation of {@link RecyclerView.ItemAnimator} provides basic
* animations on remove, add, and move events that happen to the items in
* a RecyclerView. RecyclerView uses a VerticalItemAnimator by default.
*
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
*/
public class VerticalItemAnimator extends SimpleItemAnimator {
private static final boolean DEBUG = false;
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
private ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
private ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
private ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
private static class MoveInfo {
public ViewHolder holder;
public int fromX, fromY, toX, toY;
private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
this.holder = holder;
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
}
private static class ChangeInfo {
public ViewHolder oldHolder, newHolder;
public int fromX, fromY, toX, toY;
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
this.oldHolder = oldHolder;
this.newHolder = newHolder;
}
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
this(oldHolder, newHolder);
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
@Override
public String toString() {
return "ChangeInfo{" +
"oldHolder=" + oldHolder +
", newHolder=" + newHolder +
", fromX=" + fromX +
", fromY=" + fromY +
", toX=" + toX +
", toY=" + toY +
'}';
}
}
@Override
public void runPendingAnimations() {
boolean removalsPending = !mPendingRemovals.isEmpty();
boolean movesPending = !mPendingMoves.isEmpty();
boolean changesPending = !mPendingChanges.isEmpty();
boolean additionsPending = !mPendingAdditions.isEmpty();
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
// nothing to animate
return;
}
// First, remove stuff
for (ViewHolder holder : mPendingRemovals) {
animateRemoveImpl(holder);
}
mPendingRemovals.clear();
// Next, move stuff
if (movesPending) {
final ArrayList<MoveInfo> moves = new ArrayList<>();
moves.addAll(mPendingMoves);
mMovesList.add(moves);
mPendingMoves.clear();
Runnable mover = new Runnable() {
@Override
public void run() {
for (MoveInfo moveInfo : moves) {
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
moveInfo.toX, moveInfo.toY);
}
moves.clear();
mMovesList.remove(moves);
}
};
if (removalsPending) {
View view = moves.get(0).holder.itemView;
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
} else {
mover.run();
}
}
// Next, change stuff, to run in parallel with move animations
if (changesPending) {
final ArrayList<ChangeInfo> changes = new ArrayList<>();
changes.addAll(mPendingChanges);
mChangesList.add(changes);
mPendingChanges.clear();
Runnable changer = new Runnable() {
@Override
public void run() {
for (ChangeInfo change : changes) {
animateChangeImpl(change);
}
changes.clear();
mChangesList.remove(changes);
}
};
if (removalsPending) {
ViewHolder holder = changes.get(0).oldHolder;
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
} else {
changer.run();
}
}
// Next, add stuff
if (additionsPending) {
final ArrayList<ViewHolder> additions = new ArrayList<>();
additions.addAll(mPendingAdditions);
mAdditionsList.add(additions);
mPendingAdditions.clear();
Runnable adder = new Runnable() {
public void run() {
for (ViewHolder holder : additions) {
animateAddImpl(holder);
}
additions.clear();
mAdditionsList.remove(additions);
}
};
if (removalsPending || movesPending || changesPending) {
long removeDuration = removalsPending ? getRemoveDuration() : 0;
long moveDuration = movesPending ? getMoveDuration() : 0;
long changeDuration = changesPending ? getChangeDuration() : 0;
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
View view = additions.get(0).itemView;
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
} else {
adder.run();
}
}
}
@Override
public boolean animateRemove(final ViewHolder holder) {
resetAnimation(holder);
mPendingRemovals.add(holder);
return true;
}
private void animateRemoveImpl(final ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mRemoveAnimations.add(holder);
animation.setDuration(getRemoveDuration())
.alpha(0).translationX(-holder.itemView.getWidth() / 2)
.setInterpolator(new AccelerateInterpolator()).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchRemoveStarting(holder);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationX(view, 0);
dispatchRemoveFinished(holder);
mRemoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateAdd(final ViewHolder holder) {
resetAnimation(holder);
ViewCompat.setAlpha(holder.itemView, 0);
ViewCompat.setTranslationX(holder.itemView, -holder.itemView.getWidth() / 2);
mPendingAdditions.add(holder);
return true;
}
private void animateAddImpl(final ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mAddAnimations.add(holder);
animation.alpha(1).translationX(0).setDuration(getAddDuration())
.setInterpolator(new DecelerateInterpolator()).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchAddStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationX(view, 0);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchAddFinished(holder);
mAddAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
int toX, int toY) {
final View view = holder.itemView;
fromX += ViewCompat.getTranslationX(holder.itemView);
fromY += ViewCompat.getTranslationY(holder.itemView);
int deltaX = toX - fromX;
int deltaY = toY - fromY;
if (deltaX == 0 && deltaY == 0) {
dispatchMoveFinished(holder);
return false;
}
resetAnimation(holder);
if (deltaX != 0) {
ViewCompat.setTranslationX(view, -deltaX);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, -deltaY);
}
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
return true;
}
private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
final View view = holder.itemView;
final int deltaX = toX - fromX;
final int deltaY = toY - fromY;
if (deltaX != 0) {
ViewCompat.animate(view).translationX(0);
}
if (deltaY != 0) {
ViewCompat.animate(view).translationY(0);
}
// TODO: make EndActions end listeners instead, since end actions aren't called when
// vpas are canceled (and can't end them. why?)
// need listener functionality in VPACompat for this. Ick.
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mMoveAnimations.add(holder);
animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchMoveStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
if (deltaX != 0) {
ViewCompat.setTranslationX(view, 0);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, 0);
}
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchMoveFinished(holder);
mMoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
// if (oldHolder != newHolder) {
// if (oldHolder != null) {
// dispatchChangeFinished(oldHolder, true);
// }
// if (newHolder != null) {
// dispatchChangeFinished(newHolder, false);
// }
// } else if (oldHolder != null) {
// dispatchChangeFinished(oldHolder, true);
// }
// return false;
if (oldHolder == newHolder) {
// Don't know how to run change animations when the same view holder is re-used.
// run a move animation to handle position changes.
if ((fromX - toX) == 0 && (fromY - toY) == 0) {
dispatchMoveFinished(oldHolder);
return false;
}
return animateMove(oldHolder, fromX, fromY, toX, toY);
}
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
resetAnimation(oldHolder);
int deltaX = (int) (toX - fromX - prevTranslationX);
int deltaY = (int) (toY - fromY - prevTranslationY);
// recover prev translation state after ending animation
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
if (newHolder != null) {
// carry over translation values
resetAnimation(newHolder);
ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
ViewCompat.setAlpha(newHolder.itemView, 0);
}
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
return true;
}
private void animateChangeImpl(final ChangeInfo changeInfo) {
final ViewHolder holder = changeInfo.oldHolder;
final View view = holder == null ? null : holder.itemView;
final ViewHolder newHolder = changeInfo.newHolder;
final View newView = newHolder != null ? newHolder.itemView : null;
if (view != null) {
final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
getChangeDuration());
mChangeAnimations.add(changeInfo.oldHolder);
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.oldHolder, true);
}
@Override
public void onAnimationEnd(View view) {
oldViewAnim.setListener(null);
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationX(view, 0);
ViewCompat.setTranslationY(view, 0);
dispatchChangeFinished(changeInfo.oldHolder, true);
mChangeAnimations.remove(changeInfo.oldHolder);
dispatchFinishedWhenDone();
}
}).start();
}
if (newView != null) {
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
mChangeAnimations.add(changeInfo.newHolder);
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
alpha(1).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.newHolder, false);
}
@Override
public void onAnimationEnd(View view) {
newViewAnimation.setListener(null);
ViewCompat.setAlpha(newView, 1);
ViewCompat.setTranslationX(newView, 0);
ViewCompat.setTranslationY(newView, 0);
dispatchChangeFinished(changeInfo.newHolder, false);
mChangeAnimations.remove(changeInfo.newHolder);
dispatchFinishedWhenDone();
}
}).start();
}
}
private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
for (int i = infoList.size() - 1; i >= 0; i--) {
ChangeInfo changeInfo = infoList.get(i);
if (endChangeAnimationIfNecessary(changeInfo, item)) {
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
infoList.remove(changeInfo);
}
}
}
}
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
if (changeInfo.oldHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
}
if (changeInfo.newHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
}
}
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
boolean oldItem = false;
if (changeInfo.newHolder == item) {
changeInfo.newHolder = null;
} else if (changeInfo.oldHolder == item) {
changeInfo.oldHolder = null;
oldItem = true;
} else {
return false;
}
ViewCompat.setAlpha(item.itemView, 1);
ViewCompat.setTranslationX(item.itemView, 0);
ViewCompat.setTranslationY(item.itemView, 0);
dispatchChangeFinished(item, oldItem);
return true;
}
@Override
public void endAnimation(ViewHolder item) {
final View view = item.itemView;
// this will trigger end callback which should set properties to their target values.
ViewCompat.animate(view).cancel();
// TODO if some other animations are chained to end, how do we cancel them as well?
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
MoveInfo moveInfo = mPendingMoves.get(i);
if (moveInfo.holder == item) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item);
mPendingMoves.remove(i);
}
}
endChangeAnimation(mPendingChanges, item);
if (mPendingRemovals.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchRemoveFinished(item);
}
if (mPendingAdditions.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
}
for (int i = mChangesList.size() - 1; i >= 0; i--) {
ArrayList<ChangeInfo> changes = mChangesList.get(i);
endChangeAnimation(changes, item);
if (changes.isEmpty()) {
mChangesList.remove(i);
}
}
for (int i = mMovesList.size() - 1; i >= 0; i--) {
ArrayList<MoveInfo> moves = mMovesList.get(i);
for (int j = moves.size() - 1; j >= 0; j--) {
MoveInfo moveInfo = moves.get(j);
if (moveInfo.holder == item) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item);
moves.remove(j);
if (moves.isEmpty()) {
mMovesList.remove(i);
}
break;
}
}
}
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
if (additions.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
if (additions.isEmpty()) {
mAdditionsList.remove(i);
}
}
}
// animations should be ended by the cancel above.
//noinspection PointlessBooleanExpression,ConstantConditions
if (mRemoveAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mRemoveAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mAddAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mAddAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mChangeAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mChangeAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mMoveAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mMoveAnimations list");
}
dispatchFinishedWhenDone();
}
private void resetAnimation(ViewHolder holder) {
AnimatorCompatHelper.clearInterpolator(holder.itemView);
endAnimation(holder);
}
@Override
public boolean isRunning() {
return (!mPendingAdditions.isEmpty() ||
!mPendingChanges.isEmpty() ||
!mPendingMoves.isEmpty() ||
!mPendingRemovals.isEmpty() ||
!mMoveAnimations.isEmpty() ||
!mRemoveAnimations.isEmpty() ||
!mAddAnimations.isEmpty() ||
!mChangeAnimations.isEmpty() ||
!mMovesList.isEmpty() ||
!mAdditionsList.isEmpty() ||
!mChangesList.isEmpty());
}
/**
* Check the state of currently pending and running animations. If there are none
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
* listeners.
*/
private void dispatchFinishedWhenDone() {
if (!isRunning()) {
dispatchAnimationsFinished();
}
}
@Override
public void endAnimations() {
int count = mPendingMoves.size();
for (int i = count - 1; i >= 0; i--) {
MoveInfo item = mPendingMoves.get(i);
View view = item.holder.itemView;
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item.holder);
mPendingMoves.remove(i);
}
count = mPendingRemovals.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mPendingRemovals.get(i);
dispatchRemoveFinished(item);
mPendingRemovals.remove(i);
}
count = mPendingAdditions.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mPendingAdditions.get(i);
View view = item.itemView;
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
mPendingAdditions.remove(i);
}
count = mPendingChanges.size();
for (int i = count - 1; i >= 0; i--) {
endChangeAnimationIfNecessary(mPendingChanges.get(i));
}
mPendingChanges.clear();
if (!isRunning()) {
return;
}
int listCount = mMovesList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<MoveInfo> moves = mMovesList.get(i);
count = moves.size();
for (int j = count - 1; j >= 0; j--) {
MoveInfo moveInfo = moves.get(j);
ViewHolder item = moveInfo.holder;
View view = item.itemView;
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(moveInfo.holder);
moves.remove(j);
if (moves.isEmpty()) {
mMovesList.remove(moves);
}
}
}
listCount = mAdditionsList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
count = additions.size();
for (int j = count - 1; j >= 0; j--) {
ViewHolder item = additions.get(j);
View view = item.itemView;
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
additions.remove(j);
if (additions.isEmpty()) {
mAdditionsList.remove(additions);
}
}
}
listCount = mChangesList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<ChangeInfo> changes = mChangesList.get(i);
count = changes.size();
for (int j = count - 1; j >= 0; j--) {
endChangeAnimationIfNecessary(changes.get(j));
if (changes.isEmpty()) {
mChangesList.remove(changes);
}
}
}
cancelAll(mRemoveAnimations);
cancelAll(mMoveAnimations);
cancelAll(mAddAnimations);
cancelAll(mChangeAnimations);
dispatchAnimationsFinished();
}
static void cancelAll(List<ViewHolder> viewHolders) {
for (int i = viewHolders.size() - 1; i >= 0; i--) {
ViewCompat.animate(viewHolders.get(i).itemView).cancel();
}
}
private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
@Override
public void onAnimationStart(View view) {}
@Override
public void onAnimationEnd(View view) {}
@Override
public void onAnimationCancel(View view) {}
}
}
@@ -1,30 +0,0 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.object;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import acr.browser.lightning.controller.BrowserController;
public class ClickHandler extends Handler {
private BrowserController mBrowserController;
public ClickHandler(Context context) {
try {
mBrowserController = (BrowserController) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement BrowserController");
}
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
String url = msg.getData().getString("url");
mBrowserController.longClickPage(url);
}
}
@@ -1,186 +0,0 @@
package acr.browser.lightning.object;
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.appcompat.R;
/**
* A drawable that can draw a "Drawer hamburger" menu or an Arrow and animate
* between them.
*/
public class DrawerArrowDrawable extends Drawable {
private final Paint mPaint = new Paint();
// The angle in degress that the arrow head is inclined at.
private static final float ARROW_HEAD_ANGLE = (float) Math.toRadians(45);
private final float mBarThickness;
// The length of top and bottom bars when they merge into an arrow
private final float mTopBottomArrowSize;
// The length of middle bar
private final float mBarSize;
// The length of the middle bar when arrow is shaped
private final float mMiddleArrowSize;
// The space between bars when they are parallel
private final float mBarGap;
// Whether bars should spin or not during progress
private final boolean mSpin;
// Use Path instead of canvas operations so that if color has transparency,
// overlapping sections
// wont look different
private final Path mPath = new Path();
// The reported intrinsic size of the drawable.
private final int mSize;
// Whether we should mirror animation when animation is reversed.
private boolean mVerticalMirror = false;
// The interpolated version of the original progress
private float mProgress;
/**
* @param context
* used to get the configuration for the drawable from
*/
public DrawerArrowDrawable(Context context) {
final TypedArray typedArray = context.getTheme().obtainStyledAttributes(null,
R.styleable.DrawerArrowToggle, R.attr.drawerArrowStyle,
R.style.Base_Widget_AppCompat_DrawerArrowToggle);
mPaint.setAntiAlias(true);
mPaint.setColor(typedArray.getColor(R.styleable.DrawerArrowToggle_color, 0));
mSize = typedArray.getDimensionPixelSize(R.styleable.DrawerArrowToggle_drawableSize, 0);
mBarSize = typedArray.getDimension(R.styleable.DrawerArrowToggle_barLength, 0);
mTopBottomArrowSize = typedArray.getDimension(
R.styleable.DrawerArrowToggle_arrowHeadLength, 0);
mBarThickness = typedArray.getDimension(R.styleable.DrawerArrowToggle_thickness, 0);
mBarGap = typedArray.getDimension(R.styleable.DrawerArrowToggle_gapBetweenBars, 0);
mSpin = typedArray.getBoolean(R.styleable.DrawerArrowToggle_spinBars, true);
mMiddleArrowSize = typedArray.getDimension(
R.styleable.DrawerArrowToggle_arrowShaftLength, 0);
typedArray.recycle();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.SQUARE);
mPaint.setStrokeWidth(mBarThickness);
}
protected boolean isLayoutRtl() {
return false;
}
/**
* If set, canvas is flipped when progress reached to end and going back to
* start.
*/
protected void setVerticalMirror(boolean verticalMirror) {
mVerticalMirror = verticalMirror;
}
@Override
public void draw(Canvas canvas) {
Rect bounds = getBounds();
final boolean isRtl = isLayoutRtl();
// Interpolated widths of arrow bars
final float arrowSize = lerp(mBarSize, mTopBottomArrowSize, mProgress);
final float middleBarSize = lerp(mBarSize, mMiddleArrowSize, mProgress);
// Interpolated size of middle bar
final float middleBarCut = lerp(0, mBarThickness / 2, mProgress);
// The rotation of the top and bottom bars (that make the arrow head)
final float rotation = lerp(0, ARROW_HEAD_ANGLE, mProgress);
// The whole canvas rotates as the transition happens
final float canvasRotate = lerp(isRtl ? 0 : -180, isRtl ? 180 : 0, mProgress);
final float topBottomBarOffset = lerp(mBarGap + mBarThickness, 0, mProgress);
mPath.rewind();
final float arrowEdge = -middleBarSize / 2;
// draw middle bar
mPath.moveTo(arrowEdge + middleBarCut, 0);
mPath.rLineTo(middleBarSize - middleBarCut, 0);
final float arrowWidth = Math.round(arrowSize * Math.cos(rotation));
final float arrowHeight = Math.round(arrowSize * Math.sin(rotation));
// top bar
mPath.moveTo(arrowEdge, topBottomBarOffset);
mPath.rLineTo(arrowWidth, arrowHeight);
// bottom bar
mPath.moveTo(arrowEdge, -topBottomBarOffset);
mPath.rLineTo(arrowWidth, -arrowHeight);
mPath.moveTo(0, 0);
mPath.close();
canvas.save();
// Rotate the whole canvas if spinning, if not, rotate it 180 to get
// the arrow pointing the other way for RTL.
if (mSpin) {
canvas.rotate(canvasRotate * ((mVerticalMirror ^ isRtl) ? -1 : 1), bounds.centerX(),
bounds.centerY());
} else if (isRtl) {
canvas.rotate(180, bounds.centerX(), bounds.centerY());
}
canvas.translate(bounds.centerX(), bounds.centerY());
canvas.drawPath(mPath, mPaint);
canvas.restore();
}
@Override
public void setAlpha(int i) {
mPaint.setAlpha(i);
}
// override
public boolean isAutoMirrored() {
// Draws rotated 180 degrees in RTL mode.
return true;
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
}
@Override
public int getIntrinsicHeight() {
return mSize;
}
@Override
public int getIntrinsicWidth() {
return mSize;
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
public float getProgress() {
return mProgress;
}
public void setProgress(float progress) {
mProgress = progress;
invalidateSelf();
}
/**
* Linear interpolate between a and b with parameter t.
*/
private static float lerp(float a, float b, float t) {
return a + (b - a) * t;
}
}
@@ -1,468 +0,0 @@
package acr.browser.lightning.object;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.TextView;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
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.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import acr.browser.lightning.R;
import acr.browser.lightning.database.BookmarkManager;
import acr.browser.lightning.database.HistoryDatabase;
import acr.browser.lightning.database.HistoryItem;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.ThemeUtils;
import acr.browser.lightning.utils.Utils;
public class SearchAdapter extends BaseAdapter implements Filterable {
private final List<HistoryItem> mHistory = new ArrayList<>();
private final List<HistoryItem> mBookmarks = new ArrayList<>();
private final List<HistoryItem> mSuggestions = new ArrayList<>();
private final List<HistoryItem> mFilteredList = new ArrayList<>();
private final List<HistoryItem> mAllBookmarks = new ArrayList<>();
private final Object mLock = new Object();
private HistoryDatabase mDatabaseHandler;
private final Context mContext;
private boolean mUseGoogle = true;
private boolean mIsExecuting = false;
private final boolean mDarkTheme;
private final boolean mIncognito;
private final BookmarkManager mBookmarkManager;
private static final String CACHE_FILE_TYPE = ".sgg";
private static final String ENCODING = "ISO-8859-1";
private static final long INTERVAL_DAY = 86400000;
private static final int MAX_SUGGESTIONS = 5;
private final SuggestionsComparator mComparator = new SuggestionsComparator();
private final String mSearchSubtitle;
private SearchFilter mFilter;
private final Drawable mSearchDrawable;
private final Drawable mHistoryDrawable;
private final Drawable mBookmarkDrawable;
public SearchAdapter(Context context, boolean dark, boolean incognito) {
mDatabaseHandler = HistoryDatabase.getInstance(context.getApplicationContext());
mBookmarkManager = BookmarkManager.getInstance(context.getApplicationContext());
mAllBookmarks.addAll(mBookmarkManager.getAllBookmarks(true));
mUseGoogle = PreferenceManager.getInstance().getGoogleSearchSuggestionsEnabled();
mContext = context;
mSearchSubtitle = mContext.getString(R.string.suggestion);
mDarkTheme = dark || incognito;
mIncognito = incognito;
Thread delete = new Thread(new Runnable() {
@Override
public void run() {
deleteOldCacheFiles(mContext);
}
});
mSearchDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_search, mDarkTheme);
mBookmarkDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_bookmark, mDarkTheme);
mHistoryDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_history, mDarkTheme);
delete.setPriority(Thread.MIN_PRIORITY);
delete.start();
}
private void deleteOldCacheFiles(Context context) {
File dir = new File(context.getCacheDir().toString());
String[] fileList = dir.list(new NameFilter());
long earliestTimeAllowed = System.currentTimeMillis() - INTERVAL_DAY;
for (String fileName : fileList) {
File file = new File(dir.getPath() + fileName);
if (earliestTimeAllowed > file.lastModified()) {
file.delete();
}
}
}
private class NameFilter implements FilenameFilter {
@Override
public boolean accept(File dir, String filename) {
return filename.endsWith(CACHE_FILE_TYPE);
}
}
public void refreshPreferences() {
mUseGoogle = PreferenceManager.getInstance().getGoogleSearchSuggestionsEnabled();
if (!mUseGoogle) {
synchronized (mSuggestions) {
mSuggestions.clear();
}
}
mDatabaseHandler = HistoryDatabase.getInstance(mContext.getApplicationContext());
}
public void refreshBookmarks() {
synchronized (mLock) {
mAllBookmarks.clear();
mAllBookmarks.addAll(mBookmarkManager.getAllBookmarks(true));
}
}
@Override
public int getCount() {
return mFilteredList.size();
}
@Override
public Object getItem(int position) {
return mFilteredList.get(position);
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
SuggestionHolder holder;
if (convertView == null) {
LayoutInflater inflater = ((Activity) mContext).getLayoutInflater();
convertView = inflater.inflate(R.layout.two_line_autocomplete, parent, false);
holder = new SuggestionHolder();
holder.mTitle = (TextView) convertView.findViewById(R.id.title);
holder.mUrl = (TextView) convertView.findViewById(R.id.url);
holder.mImage = (ImageView) convertView.findViewById(R.id.suggestionIcon);
convertView.setTag(holder);
} else {
holder = (SuggestionHolder) convertView.getTag();
}
HistoryItem web;
web = mFilteredList.get(position);
holder.mTitle.setText(web.getTitle());
holder.mUrl.setText(web.getUrl());
Drawable image;
switch (web.getImageId()) {
case R.drawable.ic_bookmark: {
if (mDarkTheme)
holder.mTitle.setTextColor(Color.WHITE);
image = mBookmarkDrawable;
break;
}
case R.drawable.ic_search: {
if (mDarkTheme)
holder.mTitle.setTextColor(Color.WHITE);
image = mSearchDrawable;
break;
}
case R.drawable.ic_history: {
if (mDarkTheme)
holder.mTitle.setTextColor(Color.WHITE);
image = mHistoryDrawable;
break;
}
default:
if (mDarkTheme)
holder.mTitle.setTextColor(Color.WHITE);
image = mSearchDrawable;
break;
}
holder.mImage.setImageDrawable(image);
return convertView;
}
@Override
public Filter getFilter() {
if (mFilter == null) {
mFilter = new SearchFilter();
}
return mFilter;
}
private class SearchFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
if (constraint == null) {
return results;
}
String query = constraint.toString().toLowerCase(Locale.getDefault());
if (mUseGoogle && !mIncognito && !mIsExecuting) {
new RetrieveSearchSuggestions().execute(query);
}
int counter = 0;
synchronized (mBookmarks) {
mBookmarks.clear();
synchronized (mLock) {
for (int n = 0; n < mAllBookmarks.size(); n++) {
if (counter >= 5) {
break;
}
if (mAllBookmarks.get(n).getTitle().toLowerCase(Locale.getDefault())
.startsWith(query)) {
mBookmarks.add(mAllBookmarks.get(n));
counter++;
} else if (mAllBookmarks.get(n).getUrl().contains(query)) {
mBookmarks.add(mAllBookmarks.get(n));
counter++;
}
}
}
}
if (mDatabaseHandler == null || mDatabaseHandler.isClosed()) {
mDatabaseHandler = HistoryDatabase.getInstance(mContext.getApplicationContext());
}
List<HistoryItem> historyList = mDatabaseHandler.findItemsContaining(constraint.toString());
synchronized (mHistory) {
mHistory.clear();
mHistory.addAll(historyList);
}
results.count = 1;
return results;
}
@Override
public CharSequence convertResultToString(Object resultValue) {
return ((HistoryItem) resultValue).getUrl();
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
synchronized (mFilteredList) {
mFilteredList.clear();
List<HistoryItem> filtered = getFilteredList();
Collections.sort(filtered, mComparator);
mFilteredList.addAll(filtered);
}
notifyDataSetChanged();
}
}
private class SuggestionHolder {
ImageView mImage;
TextView mTitle;
TextView mUrl;
}
private class RetrieveSearchSuggestions extends AsyncTask<String, Void, List<HistoryItem>> {
private XmlPullParserFactory mFactory;
private XmlPullParser mXpp;
@Override
protected List<HistoryItem> doInBackground(String... arg0) {
mIsExecuting = true;
List<HistoryItem> filter = new ArrayList<>();
String query = arg0[0];
try {
query = query.replace(" ", "+");
URLEncoder.encode(query, ENCODING);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
File cache = downloadSuggestionsForQuery(query);
if (!cache.exists()) {
return filter;
}
InputStream fileInput = null;
try {
fileInput = new BufferedInputStream(new FileInputStream(cache));
if (mFactory == null) {
mFactory = XmlPullParserFactory.newInstance();
mFactory.setNamespaceAware(true);
}
if (mXpp == null) {
mXpp = mFactory.newPullParser();
}
mXpp.setInput(fileInput, ENCODING);
int eventType = mXpp.getEventType();
int counter = 0;
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG && "suggestion".equals(mXpp.getName())) {
String suggestion = mXpp.getAttributeValue(null, "data");
filter.add(new HistoryItem(mSearchSubtitle + " \"" + suggestion + '"',
suggestion, R.drawable.ic_search));
counter++;
if (counter >= 5) {
break;
}
}
eventType = mXpp.next();
}
} catch (Exception e) {
return filter;
} finally {
Utils.close(fileInput);
}
return filter;
}
@Override
protected void onPostExecute(List<HistoryItem> result) {
synchronized (mSuggestions) {
mSuggestions.clear();
mSuggestions.addAll(result);
}
synchronized (mFilteredList) {
mFilteredList.clear();
List<HistoryItem> filtered = getFilteredList();
Collections.sort(filtered, mComparator);
mFilteredList.addAll(filtered);
notifyDataSetChanged();
}
mIsExecuting = false;
}
}
private File downloadSuggestionsForQuery(String query) {
File cacheFile = new File(mContext.getCacheDir(), query.hashCode() + CACHE_FILE_TYPE);
if (System.currentTimeMillis() - INTERVAL_DAY < cacheFile.lastModified()) {
return cacheFile;
}
if (!isNetworkConnected(mContext)) {
return cacheFile;
}
InputStream in = null;
FileOutputStream fos = null;
try {
URL url = new URL("http://google.com/complete/search?q=" + query
+ "&output=toolbar&hl=en");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.connect();
in = connection.getInputStream();
if (in != null) {
fos = new FileOutputStream(cacheFile);
int buffer;
while ((buffer = in.read()) != -1) {
fos.write(buffer);
}
fos.flush();
}
cacheFile.setLastModified(System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
} finally {
Utils.close(in);
Utils.close(fos);
}
return cacheFile;
}
private static boolean isNetworkConnected(Context context) {
NetworkInfo networkInfo = getActiveNetworkInfo(context);
return networkInfo != null && networkInfo.isConnected();
}
private static NetworkInfo getActiveNetworkInfo(Context context) {
ConnectivityManager connectivity = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivity == null) {
return null;
}
return connectivity.getActiveNetworkInfo();
}
//TODO Write simpler algorithm
// private List<HistoryItem> getSuggestions() {
// List<HistoryItem> filteredList = new ArrayList<>();
//
// int suggestionsSize = mSuggestions.size();
// int historySize = mHistory.size();
// int bookmarkSize = mBookmarks.size();
//
// int maxSuggestions = (bookmarkSize + historySize < 3) ? (5 - bookmarkSize - historySize) : (bookmarkSize < 2) ? (4 - bookmarkSize) : (historySize < 1) ? 3 : 2;
// int maxHistory = (suggestionsSize + bookmarkSize < 4) ? (5 - suggestionsSize - bookmarkSize) : 1;
// int maxBookmarks = (suggestionsSize + historySize < 3) ? (5 - suggestionsSize - historySize) : 2;
//
// for (int n = 0; n < bookmarkSize && n < maxBookmarks; n++) {
// filteredList.add(mBookmarks.get(n));
// }
//
// for (int n = 0; n < historySize && n < maxHistory; n++) {
// filteredList.add(mHistory.get(n));
// }
//
// for (int n = 0; n < suggestionsSize && n < maxSuggestions; n++) {
// filteredList.add(mSuggestions.get(n));
// }
// return filteredList;
// }
private List<HistoryItem> getFilteredList() {
List<HistoryItem> list = new ArrayList<>();
synchronized (mBookmarks) {
synchronized (mHistory) {
synchronized (mSuggestions) {
Iterator<HistoryItem> bookmark = mBookmarks.iterator();
Iterator<HistoryItem> history = mHistory.iterator();
Iterator<HistoryItem> suggestion = mSuggestions.listIterator();
while (list.size() < MAX_SUGGESTIONS) {
if (!bookmark.hasNext() && !suggestion.hasNext() && !history.hasNext()) {
return list;
}
if (bookmark.hasNext()) {
list.add(bookmark.next());
}
if (suggestion.hasNext() && list.size() < MAX_SUGGESTIONS) {
list.add(suggestion.next());
}
if (history.hasNext() && list.size() < MAX_SUGGESTIONS) {
list.add(history.next());
}
}
}
}
}
return list;
}
private class SuggestionsComparator implements Comparator<HistoryItem> {
@Override
public int compare(HistoryItem lhs, HistoryItem rhs) {
if (lhs.getImageId() == rhs.getImageId()) return 0;
if (lhs.getImageId() == R.drawable.ic_bookmark) return -1;
if (rhs.getImageId() == R.drawable.ic_bookmark) return 1;
if (lhs.getImageId() == R.drawable.ic_history) return -1;
return 1;
}
}
}
@@ -1,11 +1,17 @@
package acr.browser.lightning.preference;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import acr.browser.lightning.activity.BrowserApp;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.download.DownloadHandler;
@Singleton
public class PreferenceManager {
private static class Name {
@@ -14,7 +20,7 @@ public class PreferenceManager {
public static final String BLOCK_IMAGES = "blockimages";
public static final String CLEAR_CACHE_EXIT = "cache";
public static final String COOKIES = "cookies";
public static final String DOWNLOAD_DIRECTORY = "download";
public static final String DOWNLOAD_DIRECTORY = "downloadLocation";
public static final String FULL_SCREEN = "fullscreen";
public static final String HIDE_STATUS_BAR = "hidestatus";
public static final String HOMEPAGE = "home";
@@ -29,7 +35,6 @@ public class PreferenceManager {
public static final String SEARCH_URL = "searchurl";
public static final String TEXT_REFLOW = "textreflow";
public static final String TEXT_SIZE = "textsize";
public static final String URL_MEMORY = "memory";
public static final String USE_WIDE_VIEWPORT = "wideviewport";
public static final String USER_AGENT = "agentchoose";
public static final String USER_AGENT_STRING = "userAgentString";
@@ -44,10 +49,11 @@ public class PreferenceManager {
public static final String INVERT_COLORS = "invertColors";
public static final String READING_TEXT_SIZE = "readingTextSize";
public static final String THEME = "Theme";
public static final String DEFAULT_BOOKMARKS = "defaultBookmarks";
public static final String TEXT_ENCODING = "textEncoding";
public static final String CLEAR_WEBSTORAGE_EXIT = "clearWebStorageExit";
public static final String SHOW_TABS_IN_DRAWER = "showTabsInDrawer";
public static final String DO_NOT_TRACK = "doNotTrack";
public static final String IDENTIFYING_HEADERS = "removeIdentifyingHeaders";
public static final String USE_PROXY = "useProxy";
public static final String PROXY_CHOICE = "proxyChoice";
@@ -55,22 +61,17 @@ public class PreferenceManager {
public static final String USE_PROXY_PORT = "useProxyPort";
public static final String INITIAL_CHECK_FOR_TOR = "checkForTor";
public static final String INITIAL_CHECK_FOR_I2P = "checkForI2P";
public static final String LEAK_CANARY = "leakCanary";
}
private static PreferenceManager mInstance;
private SharedPreferences mPrefs;
@NonNull private final SharedPreferences mPrefs;
private static final String PREFERENCES = "settings";
public static PreferenceManager getInstance() {
if (mInstance == null) {
mInstance = new PreferenceManager();
}
return mInstance;
}
private PreferenceManager() {
mPrefs = BrowserApp.getAppContext().getSharedPreferences(PREFERENCES, 0);
@Inject
PreferenceManager(@NonNull final Context context) {
mPrefs = context.getSharedPreferences(PREFERENCES, 0);
}
public boolean getAdBlockEnabled() {
@@ -110,19 +111,16 @@ public class PreferenceManager {
}
public boolean getColorModeEnabled() {
return mPrefs.getBoolean(Name.ENABLE_COLOR_MODE, false);
return mPrefs.getBoolean(Name.ENABLE_COLOR_MODE, true);
}
public boolean getCookiesEnabled() {
return mPrefs.getBoolean(Name.COOKIES, true);
}
public boolean getDefaultBookmarks() {
return mPrefs.getBoolean(Name.DEFAULT_BOOKMARKS, true);
}
@NonNull
public String getDownloadDirectory() {
return mPrefs.getString(Name.DOWNLOAD_DIRECTORY, Environment.DIRECTORY_DOWNLOADS);
return mPrefs.getString(Name.DOWNLOAD_DIRECTORY, DownloadHandler.DEFAULT_DOWNLOAD_PATH);
}
public int getFlashSupport() {
@@ -141,6 +139,7 @@ public class PreferenceManager {
return mPrefs.getBoolean(Name.HIDE_STATUS_BAR, false);
}
@NonNull
public String getHomepage() {
return mPrefs.getString(Name.HOMEPAGE, Constants.HOMEPAGE);
}
@@ -161,10 +160,6 @@ public class PreferenceManager {
return mPrefs.getBoolean(Name.LOCATION, false);
}
public String getMemoryUrl() {
return mPrefs.getString(Name.URL_MEMORY, "");
}
public boolean getOverviewModeEnabled() {
return mPrefs.getBoolean(Name.OVERVIEW_MODE, true);
}
@@ -173,6 +168,7 @@ public class PreferenceManager {
return mPrefs.getBoolean(Name.POPUPS, true);
}
@NonNull
public String getProxyHost() {
return mPrefs.getString(Name.USE_PROXY_HOST, "localhost");
}
@@ -193,6 +189,7 @@ public class PreferenceManager {
return mPrefs.getBoolean(Name.RESTORE_LOST_TABS, true);
}
@Nullable
public String getSavedUrl() {
return mPrefs.getString(Name.SAVE_URL, null);
}
@@ -205,6 +202,7 @@ public class PreferenceManager {
return mPrefs.getInt(Name.SEARCH, 1);
}
@NonNull
public String getSearchUrl() {
return mPrefs.getString(Name.SEARCH_URL, Constants.GOOGLE_SEARCH);
}
@@ -237,7 +235,8 @@ public class PreferenceManager {
return mPrefs.getInt(Name.USER_AGENT, 1);
}
public String getUserAgentString(String def) {
@Nullable
public String getUserAgentString(@Nullable String def) {
return mPrefs.getString(Name.USER_AGENT_STRING, def);
}
@@ -245,31 +244,48 @@ public class PreferenceManager {
return mPrefs.getBoolean(Name.USE_WIDE_VIEWPORT, true);
}
@NonNull
public String getTextEncoding() {
return mPrefs.getString(Name.TEXT_ENCODING, Constants.DEFAULT_ENCODING);
}
public boolean getShowTabsInDrawer(boolean defaultValue){
public boolean getShowTabsInDrawer(boolean defaultValue) {
return mPrefs.getBoolean(Name.SHOW_TABS_IN_DRAWER, defaultValue);
}
private void putBoolean(String name, boolean value) {
public boolean getDoNotTrackEnabled() {
return mPrefs.getBoolean(Name.DO_NOT_TRACK, false);
}
public boolean getRemoveIdentifyingHeadersEnabled() {
return mPrefs.getBoolean(Name.IDENTIFYING_HEADERS, false);
}
private void putBoolean(@NonNull String name, boolean value) {
mPrefs.edit().putBoolean(name, value).apply();
}
private void putInt(String name, int value) {
private void putInt(@NonNull String name, int value) {
mPrefs.edit().putInt(name, value).apply();
}
private void putString(String name, String value) {
private void putString(@NonNull String name, @Nullable String value) {
mPrefs.edit().putString(name, value).apply();
}
public void setShowTabsInDrawer(boolean show){
public void setRemoveIdentifyingHeadersEnabled(boolean enabled) {
putBoolean(Name.IDENTIFYING_HEADERS, enabled);
}
public void setDoNotTrackEnabled(boolean doNotTrack) {
putBoolean(Name.DO_NOT_TRACK, doNotTrack);
}
public void setShowTabsInDrawer(boolean show) {
putBoolean(Name.SHOW_TABS_IN_DRAWER, show);
}
public void setTextEncoding(String encoding) {
public void setTextEncoding(@NonNull String encoding) {
putString(Name.TEXT_ENCODING, encoding);
}
@@ -317,11 +333,7 @@ public class PreferenceManager {
putBoolean(Name.COOKIES, enable);
}
public void setDefaultBookmarks(boolean show) {
putBoolean(Name.DEFAULT_BOOKMARKS, show);
}
public void setDownloadDirectory(String directory) {
public void setDownloadDirectory(@NonNull String directory) {
putString(Name.DOWNLOAD_DIRECTORY, directory);
}
@@ -341,7 +353,7 @@ public class PreferenceManager {
putBoolean(Name.HIDE_STATUS_BAR, enable);
}
public void setHomepage(String homepage) {
public void setHomepage(@NonNull String homepage) {
putString(Name.HOMEPAGE, homepage);
}
@@ -361,10 +373,6 @@ public class PreferenceManager {
putBoolean(Name.LOCATION, enable);
}
public void setMemoryUrl(String url) {
putString(Name.URL_MEMORY, url);
}
public void setOverviewModeEnabled(boolean enable) {
putBoolean(Name.OVERVIEW_MODE, enable);
}
@@ -385,7 +393,7 @@ public class PreferenceManager {
putBoolean(Name.RESTORE_LOST_TABS, enable);
}
public void setSavedUrl(String url) {
public void setSavedUrl(@Nullable String url) {
putString(Name.SAVE_URL, url);
}
@@ -397,7 +405,7 @@ public class PreferenceManager {
putInt(Name.SEARCH, choice);
}
public void setSearchUrl(String url) {
public void setSearchUrl(@NonNull String url) {
putString(Name.SEARCH_URL, url);
}
@@ -417,6 +425,14 @@ public class PreferenceManager {
putInt(Name.THEME, theme);
}
public void setUseLeakCanary(boolean useLeakCanary) {
putBoolean(Name.LEAK_CANARY, useLeakCanary);
}
public boolean getUseLeakCanary() {
return mPrefs.getBoolean(Name.LEAK_CANARY, false);
}
/**
* Valid choices:
* <ul>
@@ -432,7 +448,7 @@ public class PreferenceManager {
putInt(Name.PROXY_CHOICE, choice);
}
public void setProxyHost(String proxyHost) {
public void setProxyHost(@NonNull String proxyHost) {
putString(Name.USE_PROXY_HOST, proxyHost);
}
@@ -444,7 +460,7 @@ public class PreferenceManager {
putInt(Name.USER_AGENT, choice);
}
public void setUserAgentString(String agent) {
public void setUserAgentString(@Nullable String agent) {
putString(Name.USER_AGENT_STRING, agent);
}
@@ -0,0 +1,16 @@
package acr.browser.lightning.react;
import android.support.annotation.NonNull;
public interface Action<T> {
/**
* Should be overridden to send the subscriber
* events such as {@link Subscriber#onNext(Object)}
* or {@link Subscriber#onComplete()}.
*
* @param subscriber the subscriber that is sent in
* when the user of the Observable
* subscribes.
*/
void onSubscribe(@NonNull Subscriber<T> subscriber);
}
@@ -0,0 +1,254 @@
package acr.browser.lightning.react;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.util.concurrent.Executor;
import acr.browser.lightning.utils.Preconditions;
/**
* An RxJava implementation. This class allows work
* to be done on a certain thread and then allows
* items to be emitted on a different thread. It is
* a replacement for {@link android.os.AsyncTask}.
*
* @param <T> the type that the Observable will emit.
*/
public class Observable<T> {
private static final String TAG = Observable.class.getSimpleName();
@NonNull private final Action<T> mAction;
@Nullable private Executor mSubscriberThread;
@Nullable private Executor mObserverThread;
@NonNull private final Executor mDefault;
private Observable(@NonNull Action<T> action) {
mAction = action;
Looper looper = Looper.myLooper();
Preconditions.checkNonNull(looper);
mDefault = new ThreadExecutor(looper);
}
/**
* Static creator method that creates an Observable from the
* {@link Action} that is passed in as the parameter. Action
* must not be null.
*
* @param action the Action to perform
* @param <T> the type that will be emitted to the onSubscribe
* @return a valid non-null Observable.
*/
@NonNull
public static <T> Observable<T> create(@NonNull Action<T> action) {
Preconditions.checkNonNull(action);
return new Observable<>(action);
}
/**
* Tells the Observable what Executor that the onSubscribe
* work should run on.
*
* @param subscribeExecutor the Executor to run the work on.
* @return returns this so that calls can be conveniently chained.
*/
public Observable<T> subscribeOn(@NonNull Executor subscribeExecutor) {
mSubscriberThread = subscribeExecutor;
return this;
}
/**
* Tells the Observable what Executor the onSubscribe should observe
* the work on.
*
* @param observerExecutor the Executor to run to callback on.
* @return returns this so that calls can be conveniently chained.
*/
public Observable<T> observeOn(@NonNull Executor observerExecutor) {
mObserverThread = observerExecutor;
return this;
}
/**
* Subscribes immediately to the Observable and ignores
* all onComplete and onNext calls.
*/
public void subscribe() {
executeOnSubscriberThread(new Runnable() {
@Override
public void run() {
mAction.onSubscribe(new Subscriber<T>() {
@Override
public void unsubscribe() {}
@Override
public void onComplete() {}
@Override
public void onStart() {}
@Override
public void onError(@NonNull Throwable throwable) {}
@Override
public void onNext(T item) {}
});
}
});
}
/**
* Immediately subscribes to the Observable and starts
* sending events from the Observable to the {@link OnSubscribe}.
*
* @param onSubscribe the class that wishes to receive onNext and
* onComplete callbacks from the Observable.
*/
public Subscription subscribe(@NonNull OnSubscribe<T> onSubscribe) {
Preconditions.checkNonNull(onSubscribe);
final Subscriber<T> subscriber = new SubscriberImpl<>(onSubscribe, this);
subscriber.onStart();
executeOnSubscriberThread(new Runnable() {
@Override
public void run() {
mAction.onSubscribe(subscriber);
}
});
return subscriber;
}
private void executeOnObserverThread(@NonNull Runnable runnable) {
if (mObserverThread != null) {
mObserverThread.execute(runnable);
} else {
mDefault.execute(runnable);
}
}
private void executeOnSubscriberThread(@NonNull Runnable runnable) {
if (mSubscriberThread != null) {
mSubscriberThread.execute(runnable);
} else {
mDefault.execute(runnable);
}
}
private static class SubscriberImpl<T> implements Subscriber<T> {
@Nullable private volatile OnSubscribe<T> mOnSubscribe;
@NonNull private final Observable<T> mObservable;
private boolean mOnCompleteExecuted = false;
private boolean mOnError = false;
public SubscriberImpl(@NonNull OnSubscribe<T> onSubscribe, @NonNull Observable<T> observable) {
mOnSubscribe = onSubscribe;
mObservable = observable;
}
@Override
public void unsubscribe() {
mOnSubscribe = null;
}
@Override
public void onComplete() {
OnSubscribe<T> onSubscribe = mOnSubscribe;
if (!mOnCompleteExecuted && onSubscribe != null && !mOnError) {
mOnCompleteExecuted = true;
mObservable.executeOnObserverThread(new OnCompleteRunnable<>(onSubscribe));
} else if (!mOnError) {
Log.e(TAG, "onComplete called more than once");
throw new RuntimeException("onComplete called more than once");
}
}
@Override
public void onStart() {
OnSubscribe<T> onSubscribe = mOnSubscribe;
if (onSubscribe != null) {
mObservable.executeOnObserverThread(new OnStartRunnable<>(onSubscribe));
}
}
@Override
public void onError(@NonNull final Throwable throwable) {
OnSubscribe<T> onSubscribe = mOnSubscribe;
if (onSubscribe != null) {
mOnError = true;
mObservable.executeOnObserverThread(new OnErrorRunnable<>(onSubscribe, throwable));
}
}
@Override
public void onNext(final T item) {
OnSubscribe<T> onSubscribe = mOnSubscribe;
if (!mOnCompleteExecuted && onSubscribe != null) {
mObservable.executeOnObserverThread(new OnNextRunnable<>(onSubscribe, item));
} else {
Log.e(TAG, "onComplete has been already called, onNext should not be called");
throw new RuntimeException("onNext should not be called after onComplete has been called");
}
}
}
private static class OnCompleteRunnable<T> implements Runnable {
private final OnSubscribe<T> onSubscribe;
public OnCompleteRunnable(@NonNull OnSubscribe<T> onSubscribe) {this.onSubscribe = onSubscribe;}
@Override
public void run() {
onSubscribe.onComplete();
}
}
private static class OnNextRunnable<T> implements Runnable {
private final OnSubscribe<T> onSubscribe;
private final T item;
public OnNextRunnable(@NonNull OnSubscribe<T> onSubscribe, T item) {
this.onSubscribe = onSubscribe;
this.item = item;
}
@Override
public void run() {
onSubscribe.onNext(item);
}
}
private static class OnErrorRunnable<T> implements Runnable {
private final OnSubscribe<T> onSubscribe;
private final Throwable throwable;
public OnErrorRunnable(@NonNull OnSubscribe<T> onSubscribe, @NonNull Throwable throwable) {
this.onSubscribe = onSubscribe;
this.throwable = throwable;
}
@Override
public void run() {
onSubscribe.onError(throwable);
}
}
private static class OnStartRunnable<T> implements Runnable {
private final OnSubscribe<T> onSubscribe;
public OnStartRunnable(@NonNull OnSubscribe<T> onSubscribe) {this.onSubscribe = onSubscribe;}
@Override
public void run() {
onSubscribe.onStart();
}
}
}
@@ -0,0 +1,47 @@
package acr.browser.lightning.react;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public abstract class OnSubscribe<T> {
/**
* Called when the observable
* runs into an error that will
* cause it to abort and not finish.
* Receiving this callback means that
* the observable is dead and no
* {@link #onComplete()} or {@link #onNext(Object)}
* callbacks will be called.
*
* @param throwable an optional throwable that could
* be sent.
*/
public void onError(@NonNull Throwable throwable) {}
/**
* Called before the observer begins
* to process and emit items or complete.
*/
public void onStart() {}
/**
* Called when the Observer emits an
* item. It can be called multiple times.
* It cannot be called after onComplete
* has been called.
*
* @param item the item that has been emitted,
* can be null.
*/
public void onNext(@Nullable T item) {}
/**
* This method is called when the observer is
* finished sending the subscriber events. It
* is guaranteed that no other methods will be
* called on the OnSubscribe after this method
* has been called.
*/
public void onComplete() {}
}
@@ -0,0 +1,46 @@
package acr.browser.lightning.react;
import android.os.Looper;
import android.support.annotation.NonNull;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class Schedulers {
private static final Executor sWorker = Executors.newFixedThreadPool(4);
private static final Executor sIOWorker = Executors.newSingleThreadExecutor();
private static final Executor sMain = new ThreadExecutor(Looper.getMainLooper());
/**
* The worker thread executor, will
* execute work on any one of multiple
* threads.
*
* @return a non-null executor.
*/
@NonNull
public static Executor worker() {
return sWorker;
}
/**
* The main thread.
*
* @return a non-null executor that does work on the main thread.
*/
@NonNull
public static Executor main() {
return sMain;
}
/**
* The io thread.
*
* @return a non-null executor that does
* work on a single thread off the main thread.
*/
@NonNull
public static Executor io() {
return sIOWorker;
}
}
@@ -0,0 +1,51 @@
package acr.browser.lightning.react;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public interface Subscriber<T> extends Subscription {
/**
* Called immediately upon subscribing
* and before the Observable begins
* emitting items. This should not be
* called by the creator of the Observable
* and is rather called internally by the
* Observable class itself.
*/
void onStart();
/**
* Called when the observable
* runs into an error that will
* cause it to abort and not finish.
* Receiving this callback means that
* the observable is dead and no
* {@link #onComplete()} or {@link #onNext(Object)}
* callbacks will be called.
*
* @param throwable an optional throwable that could
* be sent.
*/
void onError(@NonNull Throwable throwable);
/**
* Called when the Observer emits an
* item. It can be called multiple times.
* It cannot be called after onComplete
* has been called.
*
* @param item the item that has been emitted,
* can be null.
*/
void onNext(@Nullable T item);
/**
* This method is called when the observer is
* finished sending the subscriber events. It
* is guaranteed that no other methods will be
* called on the OnSubscribe after this method
* has been called.
*/
void onComplete();
}
@@ -0,0 +1,7 @@
package acr.browser.lightning.react;
public interface Subscription {
void unsubscribe();
}
@@ -0,0 +1,21 @@
package acr.browser.lightning.react;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import java.util.concurrent.Executor;
class ThreadExecutor implements Executor {
private final Handler mHandler;
public ThreadExecutor(@NonNull Looper looper) {
mHandler = new Handler(looper);
}
@Override
public void execute(@NonNull Runnable command) {
mHandler.post(command);
}
}
@@ -385,16 +385,16 @@ public class ArticleTextExtractor {
// System.out.println("date modified element " + elem.toString());
}
if ("".equals(dateStr)) {
if (dateStr.isEmpty()) {
dateStr = SHelper.innerTrim(doc.select("meta[name=utime]").attr("content"));
}
if ("".equals(dateStr)) {
if (dateStr.isEmpty()) {
dateStr = SHelper.innerTrim(doc.select("meta[name=pdate]").attr("content"));
}
if ("".equals(dateStr)) {
if (dateStr.isEmpty()) {
dateStr = SHelper.innerTrim(doc.select("meta[property=article:published]").attr("content"));
}
if ("".equals(dateStr)) {
if (dateStr.isEmpty()) {
return parseDate(dateStr);
}
@@ -491,10 +491,7 @@ public class ArticleTextExtractor {
Element el = elems.get(0);
if (el.hasAttr("content")) {
dateStr = el.attr("content");
Date parsedDate = parseDate(dateStr);
if (parsedDate != null) {
return parsedDate;
}
return parseDate(dateStr);
}
}
@@ -686,14 +683,12 @@ public class ArticleTextExtractor {
private static Collection<String> extractKeywords(Document doc) {
String content = SHelper.innerTrim(doc.select("head meta[name=keywords]").attr("content"));
if (content != null) {
if (content.startsWith("[") && content.endsWith("]"))
content = content.substring(1, content.length() - 1);
if (content.startsWith("[") && content.endsWith("]"))
content = content.substring(1, content.length() - 1);
String[] split = content.split("\\s*,\\s*");
if (split.length > 1 || (split.length > 0 && split[0] != null && !split[0].isEmpty()))
return Arrays.asList(split);
}
String[] split = content.split("\\s*,\\s*");
if (split.length > 1 || (split.length > 0 && split[0] != null && !split[0].isEmpty()))
return Arrays.asList(split);
return Collections.emptyList();
}
@@ -736,8 +731,7 @@ public class ArticleTextExtractor {
}
private static String extractType(Document doc) {
String type = SHelper.innerTrim(doc.select("head meta[property=og:type]").attr("content"));
return type;
return SHelper.innerTrim(doc.select("head meta[property=og:type]").attr("content"));
}
private static String extractSitename(Document doc) {
@@ -969,7 +963,7 @@ public class ArticleTextExtractor {
return weight;
}
private Element determineImageSource(Element el, List<ImageResult> images) {
private static Element determineImageSource(Element el, List<ImageResult> images) {
int maxWeight = 0;
Element maxNode = null;
Elements els = el.select("img");
@@ -1143,13 +1137,14 @@ public class ArticleTextExtractor {
}
private static String cleanTitle(String title) {
StringBuilder res = new StringBuilder();
// int index = title.lastIndexOf("|");
// if (index > 0 && title.length() / 2 < index)
// title = title.substring(0, index + 1);
int counter = 0;
String[] strs = title.split("\\|");
StringBuilder res = new StringBuilder(strs.length);
for (String part : strs) {
if (IGNORED_TITLE_PARTS.contains(part.toLowerCase().trim()))
continue;
@@ -1190,7 +1185,7 @@ public class ArticleTextExtractor {
charlen = 4;
} else if (c <= 0xdfff) {
charlen = 0;
} else if (c <= 0xffff) {
} else {
charlen = 3;
}
if (resultlen + charlen > length) {
@@ -1208,7 +1203,7 @@ public class ArticleTextExtractor {
*
* @author Chris Alexander, chris@chris-alexander.co.uk
*/
private class ImageComparator implements Comparator<ImageResult> {
private static class ImageComparator implements Comparator<ImageResult> {
@Override
public int compare(ImageResult o1, ImageResult o2) {
@@ -35,9 +35,9 @@ import acr.browser.lightning.constant.Constants;
*/
public class Converter {
public final static String UTF8 = "UTF-8";
public final static String ISO = "ISO-8859-1";
public final static int K2 = 2048;
private final static String UTF8 = "UTF-8";
private final static String ISO = "ISO-8859-1";
private final static int K2 = 2048;
private int maxBytes = 1000000 / 2;
private String encoding;
private String url;
@@ -99,7 +99,7 @@ public class Converter {
* The max bytes that we want to read from the input stream
* @return String
*/
public String streamToString(InputStream is, int maxBytes, String enc) {
private String streamToString(InputStream is, int maxBytes, String enc) {
encoding = enc;
// Http 1.1. standard is iso-8859-1 not utf8 :(
// but we force utf-8 as youtube assumes it ;)
@@ -181,8 +181,8 @@ public class Converter {
*
* @throws IOException
*/
protected static String detectCharset(String key, ByteArrayOutputStream bos, BufferedInputStream in,
String enc) throws IOException {
private static String detectCharset(String key, ByteArrayOutputStream bos, BufferedInputStream in,
String enc) throws IOException {
// Grab better encoding from stream
byte[] arr = new byte[K2];
@@ -204,24 +204,24 @@ public class Converter {
int lastEncIndex;
if (startChar == '\'')
// if we have charset='something'
lastEncIndex = str.indexOf("'", ++encIndex + clength);
lastEncIndex = str.indexOf('\'', ++encIndex + clength);
else if (startChar == '\"')
// if we have charset="something"
lastEncIndex = str.indexOf("\"", ++encIndex + clength);
lastEncIndex = str.indexOf('\"', ++encIndex + clength);
else {
// if we have "text/html; charset=utf-8"
int first = str.indexOf("\"", encIndex + clength);
int first = str.indexOf('\"', encIndex + clength);
if (first < 0)
first = Integer.MAX_VALUE;
// or "text/html; charset=utf-8 "
int sec = str.indexOf(" ", encIndex + clength);
int sec = str.indexOf(' ', encIndex + clength);
if (sec < 0)
sec = Integer.MAX_VALUE;
lastEncIndex = Math.min(first, sec);
// or "text/html; charset=utf-8 '
int third = str.indexOf("'", encIndex + clength);
int third = str.indexOf('\'', encIndex + clength);
if (third > 0)
lastEncIndex = Math.min(lastEncIndex, third);
}
@@ -28,10 +28,13 @@ import java.net.URL;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import acr.browser.lightning.utils.Utils;
/**
* Class to fetch articles. This class is thread safe.
*
@@ -39,6 +42,8 @@ import java.util.zip.InflaterInputStream;
*/
public class HtmlFetcher {
private static final Pattern SPACE = Pattern.compile(" ");
static {
SHelper.enableCookieMgmt();
SHelper.enableUserAgentOverwrite();
@@ -46,28 +51,36 @@ public class HtmlFetcher {
}
public static void main(String[] args) throws Exception {
BufferedReader reader = new BufferedReader(new FileReader("urls.txt"));
String line;
Set<String> existing = new LinkedHashSet<>();
while ((line = reader.readLine()) != null) {
int index1 = line.indexOf("\"");
int index2 = line.indexOf("\"", index1 + 1);
String url = line.substring(index1 + 1, index2);
String domainStr = SHelper.extractDomain(url, true);
String counterStr = "";
// TODO more similarities
if (existing.contains(domainStr))
counterStr = "2";
else
existing.add(domainStr);
BufferedReader reader = null;
BufferedWriter writer = null;
try {
String html = new HtmlFetcher().fetchAsString(url, 2000);
String outFile = domainStr + counterStr + ".html";
BufferedWriter writer = new BufferedWriter(new FileWriter(outFile));
writer.write(html);
writer.close();
//noinspection IOResourceOpenedButNotSafelyClosed
reader = new BufferedReader(new FileReader("urls.txt"));
String line;
Set<String> existing = new LinkedHashSet<>();
while ((line = reader.readLine()) != null) {
int index1 = line.indexOf('\"');
int index2 = line.indexOf('\"', index1 + 1);
String url = line.substring(index1 + 1, index2);
String domainStr = SHelper.extractDomain(url, true);
String counterStr = "";
// TODO more similarities
if (existing.contains(domainStr))
counterStr = "2";
else
existing.add(domainStr);
String html = new HtmlFetcher().fetchAsString(url, 2000);
String outFile = domainStr + counterStr + ".html";
//noinspection IOResourceOpenedButNotSafelyClosed
writer = new BufferedWriter(new FileWriter(outFile));
writer.write(html);
}
} finally {
Utils.close(reader);
Utils.close(writer);
}
reader.close();
}
private String referrer = "http://jetsli.de/crawler";
@@ -203,8 +216,9 @@ public class HtmlFetcher {
}
// main workhorse to call externally
public JResult fetchAndExtract(String url, int timeout, boolean resolve,
int maxContentSize, boolean forceReload) throws Exception {
@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
private JResult fetchAndExtract(String url, int timeout, boolean resolve,
int maxContentSize, boolean forceReload) throws Exception {
String originalUrl = url;
url = SHelper.removeHashbang(url);
String gUrl = SHelper.getUrlFromUglyGoogleRedirect(url);
@@ -297,7 +311,7 @@ public class HtmlFetcher {
}
// Ugly hack to break free from any cached versions, a few URLs required this.
public static String getURLtoBreakCache(String url) {
private static String getURLtoBreakCache(String url) {
try {
URL aURL = new URL(url);
if (aURL.getQuery() != null && aURL.getQuery().isEmpty()) {
@@ -310,7 +324,7 @@ public class HtmlFetcher {
}
}
public String lessText(String text) {
private String lessText(String text) {
if (text == null)
return "";
@@ -324,14 +338,14 @@ public class HtmlFetcher {
return SHelper.useDomainOfFirstArg4Second(url, urlOrPath);
}
public String fetchAsString(String urlAsString, int timeout)
throws MalformedURLException, IOException {
private String fetchAsString(String urlAsString, int timeout)
throws IOException {
return fetchAsString(urlAsString, timeout, true);
}
// main routine to get raw webpage content
public String fetchAsString(String urlAsString, int timeout, boolean includeSomeGooseOptions)
throws MalformedURLException, IOException {
private String fetchAsString(String urlAsString, int timeout, boolean includeSomeGooseOptions)
throws IOException {
HttpURLConnection hConn = createUrlConnection(urlAsString, timeout, includeSomeGooseOptions);
hConn.setInstanceFollowRedirects(true);
String encoding = hConn.getContentEncoding();
@@ -348,7 +362,7 @@ public class HtmlFetcher {
return createConverter(urlAsString).streamToString(is, enc);
}
public static Converter createConverter(String url) {
private static Converter createConverter(String url) {
return new Converter(url);
}
@@ -360,8 +374,8 @@ public class HtmlFetcher {
* @return the resolved url if any. Or null if it couldn't resolve the url
* (within the specified time) or the same url if response code is OK
*/
public String getResolvedUrl(String urlAsString, int timeout,
int num_redirects) {
private String getResolvedUrl(String urlAsString, int timeout,
int num_redirects) {
String newUrl = null;
int responseCode = -1;
try {
@@ -380,10 +394,10 @@ public class HtmlFetcher {
newUrl = hConn.getHeaderField("Location");
// Note that the max recursion level is 5.
if (responseCode / 100 == 3 && newUrl != null && num_redirects < 5) {
newUrl = newUrl.replaceAll(" ", "+");
newUrl = SPACE.matcher(newUrl).replaceAll("+");
// some services use (none-standard) utf8 in their location header
if (urlAsString.startsWith("http://bit.ly")
|| urlAsString.startsWith("http://is.gd"))
if (urlAsString.contains("://bit.ly")
|| urlAsString.contains("://is.gd"))
newUrl = encodeUriFromHeader(newUrl);
// AP: This code is not longer need, instead we always follow
@@ -412,8 +426,8 @@ public class HtmlFetcher {
* to non-ASCII characters. Workaround for broken origin servers that send
* UTF-8 in the Location: header.
*/
static String encodeUriFromHeader(String badLocation) {
StringBuilder sb = new StringBuilder();
private static String encodeUriFromHeader(String badLocation) {
StringBuilder sb = new StringBuilder(badLocation.length());
for (char ch : badLocation.toCharArray()) {
if (ch < (char) 128) {
@@ -427,8 +441,8 @@ public class HtmlFetcher {
return sb.toString();
}
protected HttpURLConnection createUrlConnection(String urlAsStr, int timeout,
boolean includeSomeGooseOptions) throws MalformedURLException, IOException {
private HttpURLConnection createUrlConnection(String urlAsStr, int timeout,
boolean includeSomeGooseOptions) throws IOException {
URL url = new URL(urlAsStr);
//using proxy may increase latency
HttpURLConnection hConn = (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);
@@ -7,15 +7,15 @@ import org.jsoup.nodes.Element;
*
* @author Chris Alexander, chris@chris-alexander.co.uk
*/
public class ImageResult {
class ImageResult {
public final String src;
private final String src;
public final Integer weight;
public final String title;
public final int height;
public final int width;
public final String alt;
public final boolean noFollow;
private final String title;
private final int height;
private final int width;
private final String alt;
private final boolean noFollow;
public Element element;
public ImageResult(String src, Integer weight, String title, int height, int width, String alt,
@@ -47,7 +47,7 @@ public class JResult implements Serializable {
private Date date;
private Collection<String> keywords;
private List<ImageResult> images = null;
private List<Map<String, String>> links = new ArrayList<>();
private final List<Map<String, String>> links = new ArrayList<>();
private String type;
private String sitename;
private String language;
@@ -230,7 +230,7 @@ public class JResult implements Serializable {
}
public void addLink(String url, String text, Integer pos) {
Map link = new HashMap();
Map<String, String> link = new HashMap<>();
link.put("url", url);
link.put("text", text);
link.put("offset", String.valueOf(pos));
@@ -1,12 +1,12 @@
/**
* Copyright (C) 2010 Peter Karich <>
*
* <p/>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
*
* <p/>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -20,7 +20,7 @@ import java.util.Map;
/**
* Simple impl of Map.Entry. So that we can have ordered maps.
*
*
* @author Peter Karich, peat_hal at users dot sourceforge dot
* net
*/
@@ -60,13 +60,12 @@ public class MapEntry<K, V> implements Map.Entry<K, V>, Serializable {
public boolean equals(Object obj) {
if (obj == null)
return false;
if (getClass() != obj.getClass())
if (!(obj instanceof Map<?, ?>))
return false;
final MapEntry<K, V> other = (MapEntry<K, V>) obj;
if (this.key != other.key && (this.key == null || !this.key.equals(other.key)))
return false;
return !(this.value != other.value && (this.value == null || !this.value
.equals(other.value)));
final MapEntry<?, ?> other = (MapEntry<?, ?>) obj;
return !(this.key != other.key && (this.key == null || !this.key.equals(other.key))) &&
!(this.value != other.value && (this.value == null || !this.value.equals(other.value)));
}
@Override
@@ -23,7 +23,7 @@ public class OutputFormatter {
private static final int MIN_FIRST_PARAGRAPH_TEXT = 50; // Min size of first paragraph
private static final int MIN_PARAGRAPH_TEXT = 30; // Min size of any other paragraphs
private static final List<String> NODES_TO_REPLACE = Arrays.asList("strong", "b", "i");
private Pattern unlikelyPattern = Pattern.compile("display\\:none|visibility\\:hidden");
private Pattern unlikelyPattern = Pattern.compile("display:none|visibility:hidden");
private final int minFirstParagraphText;
private final int minParagraphText;
private final List<String> nodesToReplace;
@@ -41,8 +41,8 @@ public class OutputFormatter {
this(minFirstParagraphText, minParagraphText, NODES_TO_REPLACE);
}
public OutputFormatter(int minFirstParagraphText, int minParagraphText,
List<String> nodesToReplace) {
private OutputFormatter(int minFirstParagraphText, int minParagraphText,
List<String> nodesToReplace) {
this.minFirstParagraphText = minFirstParagraphText;
this.minParagraphText = minParagraphText;
this.nodesToReplace = nodesToReplace;
@@ -91,7 +91,7 @@ public class OutputFormatter {
* If there are elements inside our top node that have a negative gravity
* score remove them
*/
protected void removeNodesWithNegativeScores(Element topNode) {
private void removeNodesWithNegativeScores(Element topNode) {
Elements gravityItems = topNode.select("*[gravityScore]");
for (Element item : gravityItems) {
int score = getScore(item);
@@ -102,7 +102,7 @@ public class OutputFormatter {
}
}
protected int append(Element node, StringBuilder sb, String tagName) {
private int append(Element node, StringBuilder sb, String tagName) {
int countOfP = 0; // Number of P elements in the article
int paragraphWithTextIndex = 0;
// is select more costly then getElementsByTag?
@@ -134,14 +134,14 @@ public class OutputFormatter {
return countOfP;
}
protected static void setParagraphIndex(Element node, String tagName) {
private static void setParagraphIndex(Element node, String tagName) {
int paragraphIndex = 0;
for (Element e : node.select(tagName)) {
e.attr("paragraphIndex", Integer.toString(paragraphIndex++));
}
}
protected int getMinParagraph(int paragraphIndex) {
private int getMinParagraph(int paragraphIndex) {
if (paragraphIndex < 1) {
return minFirstParagraphText;
} else {
@@ -149,7 +149,7 @@ public class OutputFormatter {
}
}
protected static int getParagraphIndex(Element el) {
private static int getParagraphIndex(Element el) {
try {
return Integer.parseInt(el.attr("paragraphIndex"));
} catch (NumberFormatException ex) {
@@ -157,7 +157,7 @@ public class OutputFormatter {
}
}
protected static int getScore(Element el) {
private static int getScore(Element el) {
try {
return Integer.parseInt(el.attr("gravityScore"));
} catch (Exception ex) {
@@ -165,7 +165,7 @@ public class OutputFormatter {
}
}
boolean unlikely(Node e) {
private boolean unlikely(Node e) {
if (e.attr("class") != null && e.attr("class").toLowerCase().contains("caption"))
return true;
@@ -174,7 +174,7 @@ public class OutputFormatter {
return unlikelyPattern.matcher(style).find() || unlikelyPattern.matcher(clazz).find();
}
void appendTextSkipHidden(Element e, StringBuilder accum, int indent) {
private void appendTextSkipHidden(Element e, StringBuilder accum, int indent) {
for (Node child : e.childNodes()) {
if (unlikely(child)) {
continue;
@@ -195,17 +195,17 @@ public class OutputFormatter {
}
}
static boolean lastCharIsWhitespace(StringBuilder accum) {
private static boolean lastCharIsWhitespace(StringBuilder accum) {
return accum.length() != 0 && Character.isWhitespace(accum.charAt(accum.length() - 1));
}
protected String node2Text(Element el) {
private String node2Text(Element el) {
StringBuilder sb = new StringBuilder(200);
appendTextSkipHidden(el, sb, 0);
return sb.toString();
}
public OutputFormatter setUnlikelyPattern(String unlikelyPattern) {
private OutputFormatter setUnlikelyPattern(String unlikelyPattern) {
this.unlikelyPattern = Pattern.compile(unlikelyPattern);
return this;
}
@@ -39,9 +39,9 @@ import javax.net.ssl.X509TrustManager;
/**
* @author Peter Karich
*/
public class SHelper {
class SHelper {
public static final String UTF8 = "UTF-8";
private static final String UTF8 = "UTF-8";
private static final Pattern SPACE = Pattern.compile(" ");
public static String replaceSpaces(String url) {
@@ -72,9 +72,9 @@ public class SHelper {
if (str.isEmpty())
return "";
StringBuilder sb = new StringBuilder();
StringBuilder sb = new StringBuilder(str.length());
boolean previousSpace = false;
for (int i = 0; i < str.length(); i++) {
for (int i = 0, length = str.length(); i < length; i++) {
char c = str.charAt(i);
if (c == ' ' || (int) c == 9 || c == '\n') {
previousSpace = true;
@@ -95,7 +95,7 @@ public class SHelper {
* invalid encoding character occurs.
*/
public static String encodingCleanup(String str) {
StringBuilder sb = new StringBuilder();
StringBuilder sb = new StringBuilder(str.length());
boolean startedWithCorrectString = false;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
@@ -122,7 +122,7 @@ public class SHelper {
return str1.substring(res[0], res[1]);
}
public static int[] longestSubstring(String str1, String str2) {
private static int[] longestSubstring(String str1, String str2) {
if (str1 == null || str1.isEmpty() || str2 == null || str2.isEmpty())
return null;
@@ -193,7 +193,7 @@ public class SHelper {
url = url.substring("m.".length());
}
int slashIndex = url.indexOf("/");
int slashIndex = url.indexOf('/');
if (slashIndex > 0)
url = url.substring(0, slashIndex);
@@ -251,9 +251,9 @@ public class SHelper {
}
public static String getUrlFromUglyGoogleRedirect(String url) {
if (url.startsWith("http://www.google.com/url?")) {
url = url.substring("http://www.google.com/url?".length());
String arr[] = urlDecode(url).split("\\&");
if (url.startsWith("https://www.google.com/url?")) {
url = url.substring("https://www.google.com/url?".length());
String arr[] = urlDecode(url).split("&");
for (String str : arr) {
if (str.startsWith("q="))
return str.substring("q=".length());
@@ -264,8 +264,8 @@ public class SHelper {
}
public static String getUrlFromUglyFacebookRedirect(String url) {
if (url.startsWith("http://www.facebook.com/l.php?u=")) {
url = url.substring("http://www.facebook.com/l.php?u=".length());
if (url.startsWith("https://www.facebook.com/l.php?u=")) {
url = url.substring("https://www.facebook.com/l.php?u=".length());
return urlDecode(url);
}
@@ -280,7 +280,7 @@ public class SHelper {
}
}
public static String urlDecode(String str) {
private static String urlDecode(String str) {
try {
return URLDecoder.decode(str, UTF8);
} catch (UnsupportedEncodingException ex) {
@@ -300,8 +300,8 @@ public class SHelper {
return printNode(root, 0);
}
public static String printNode(Element root, int indentation) {
StringBuilder sb = new StringBuilder();
private static String printNode(Element root, int indentation) {
StringBuilder sb = new StringBuilder(indentation);
for (int i = 0; i < indentation; i++) {
sb.append(' ');
}
@@ -5,14 +5,14 @@ import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.support.annotation.NonNull;
public class NetworkReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
}
public void onReceive(Context context, Intent intent) {}
public static boolean isConnected(Context context) {
public static boolean isConnected(@NonNull Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null)
return false;
@@ -0,0 +1,201 @@
package acr.browser.lightning.search;
import android.app.Application;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
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.regex.Pattern;
import acr.browser.lightning.R;
import acr.browser.lightning.database.HistoryItem;
import acr.browser.lightning.utils.Utils;
class RetrieveSuggestionsTask extends AsyncTask<Void, Void, List<HistoryItem>> {
private static final String TAG = RetrieveSuggestionsTask.class.getSimpleName();
private static final Pattern SPACE_PATTERN = Pattern.compile(" ", Pattern.LITERAL);
private static final String CACHE_FILE_TYPE = ".sgg";
private static final String ENCODING = "ISO-8859-1";
private static final long INTERVAL_DAY = 86400000;
private static final String DEFAULT_LANGUAGE = "en";
@Nullable private static XmlPullParser sXpp;
@Nullable private static String sLanguage;
@NonNull private final WeakReference<SuggestionsResult> mResultCallback;
@NonNull private final Application mApplication;
@NonNull private final String mSearchSubtitle;
@NonNull private String mQuery;
public RetrieveSuggestionsTask(@NonNull String query,
@NonNull SuggestionsResult callback,
@NonNull Application application) {
mQuery = query;
mResultCallback = new WeakReference<>(callback);
mApplication = application;
mSearchSubtitle = mApplication.getString(R.string.suggestion);
}
@NonNull
private static synchronized String getLanguage() {
if (sLanguage == null) {
sLanguage = Locale.getDefault().getLanguage();
}
if (TextUtils.isEmpty(sLanguage)) {
sLanguage = DEFAULT_LANGUAGE;
}
return sLanguage;
}
@NonNull
private static synchronized XmlPullParser getParser() throws XmlPullParserException {
if (sXpp == null) {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
sXpp = factory.newPullParser();
}
return sXpp;
}
@NonNull
@Override
protected List<HistoryItem> doInBackground(Void... voids) {
List<HistoryItem> filter = new ArrayList<>(5);
try {
mQuery = SPACE_PATTERN.matcher(mQuery).replaceAll("+");
URLEncoder.encode(mQuery, ENCODING);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
File cache = downloadSuggestionsForQuery(mQuery, getLanguage(), mApplication);
if (!cache.exists()) {
return filter;
}
InputStream fileInput = null;
try {
fileInput = new BufferedInputStream(new FileInputStream(cache));
XmlPullParser parser = getParser();
parser.setInput(fileInput, ENCODING);
int eventType = parser.getEventType();
int counter = 0;
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG && "suggestion".equals(parser.getName())) {
String suggestion = parser.getAttributeValue(null, "data");
filter.add(new HistoryItem(mSearchSubtitle + " \"" + suggestion + '"',
suggestion, R.drawable.ic_search));
counter++;
if (counter >= 5) {
break;
}
}
eventType = parser.next();
}
} catch (Exception e) {
return filter;
} finally {
Utils.close(fileInput);
}
return filter;
}
@Override
protected void onPostExecute(@NonNull List<HistoryItem> result) {
SuggestionsResult callback = mResultCallback.get();
if (callback != null) {
callback.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 static File downloadSuggestionsForQuery(@NonNull String query, String language, @NonNull Application app) {
File cacheFile = new File(app.getCacheDir(), query.hashCode() + CACHE_FILE_TYPE);
if (System.currentTimeMillis() - INTERVAL_DAY < cacheFile.lastModified()) {
return cacheFile;
}
if (!isNetworkConnected(app)) {
return cacheFile;
}
InputStream in = null;
FileOutputStream fos = null;
try {
// Old API that doesn't support HTTPS
// http://google.com/complete/search?q= + query + &output=toolbar&hl= + language
URL url = new URL("https://suggestqueries.google.com/complete/search?output=toolbar&hl="
+ language + "&q=" + query);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
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) {
//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();
}
}
@@ -0,0 +1,331 @@
package acr.browser.lightning.search;
import android.app.Application;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.TextView;
import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
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.preference.PreferenceManager;
import acr.browser.lightning.utils.ThemeUtils;
public class SuggestionsAdapter extends BaseAdapter implements Filterable, SuggestionsResult {
private static final String TAG = SuggestionsAdapter.class.getSimpleName();
private final List<HistoryItem> mHistory = new ArrayList<>(5);
private final List<HistoryItem> mBookmarks = new ArrayList<>(5);
private final List<HistoryItem> mSuggestions = new ArrayList<>(5);
private final List<HistoryItem> mFilteredList = new ArrayList<>(5);
private final List<HistoryItem> mAllBookmarks = new ArrayList<>(5);
private boolean mUseGoogle = true;
private boolean mIsExecuting = false;
private final boolean mDarkTheme;
private final boolean mIncognito;
private static final String CACHE_FILE_TYPE = ".sgg";
private static final long INTERVAL_DAY = 86400000;
private static final int MAX_SUGGESTIONS = 5;
private static final SuggestionsComparator sComparator = new SuggestionsComparator();
@NonNull private final Context mContext;
@Nullable private SearchFilter mFilter;
@NonNull private final Drawable mSearchDrawable;
@NonNull private final Drawable mHistoryDrawable;
@NonNull private final Drawable mBookmarkDrawable;
@Inject HistoryDatabase mDatabaseHandler;
@Inject BookmarkManager mBookmarkManager;
@Inject PreferenceManager mPreferenceManager;
public SuggestionsAdapter(@NonNull Context context, boolean dark, boolean incognito) {
BrowserApp.getAppComponent().inject(this);
mAllBookmarks.addAll(mBookmarkManager.getAllBookmarks(true));
mUseGoogle = mPreferenceManager.getGoogleSearchSuggestionsEnabled();
mContext = context;
mDarkTheme = dark || incognito;
mIncognito = incognito;
BrowserApp.getTaskThread().execute(new ClearCacheRunnable(BrowserApp.get(context)));
mSearchDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_search, mDarkTheme);
mBookmarkDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_bookmark, mDarkTheme);
mHistoryDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_history, mDarkTheme);
}
public void refreshPreferences() {
mUseGoogle = mPreferenceManager.getGoogleSearchSuggestionsEnabled();
if (!mUseGoogle) {
synchronized (mSuggestions) {
mSuggestions.clear();
}
}
}
public void refreshBookmarks() {
synchronized (SuggestionsAdapter.this) {
mAllBookmarks.clear();
mAllBookmarks.addAll(mBookmarkManager.getAllBookmarks(true));
}
}
@Override
public int getCount() {
return mFilteredList.size();
}
@Override
public Object getItem(int position) {
return mFilteredList.get(position);
}
@Override
public long getItemId(int position) {
return 0;
}
private static class SuggestionHolder {
ImageView mImage;
TextView mTitle;
TextView mUrl;
}
@Nullable
@Override
public View getView(int position, @Nullable View convertView, ViewGroup parent) {
SuggestionHolder holder;
if (convertView == null) {
LayoutInflater inflater = LayoutInflater.from(mContext);
convertView = inflater.inflate(R.layout.two_line_autocomplete, parent, false);
holder = new SuggestionHolder();
holder.mTitle = (TextView) convertView.findViewById(R.id.title);
holder.mUrl = (TextView) convertView.findViewById(R.id.url);
holder.mImage = (ImageView) convertView.findViewById(R.id.suggestionIcon);
convertView.setTag(holder);
} else {
holder = (SuggestionHolder) convertView.getTag();
}
HistoryItem web;
web = mFilteredList.get(position);
holder.mTitle.setText(web.getTitle());
holder.mUrl.setText(web.getUrl());
Drawable image;
switch (web.getImageId()) {
case R.drawable.ic_bookmark: {
if (mDarkTheme)
holder.mTitle.setTextColor(Color.WHITE);
image = mBookmarkDrawable;
break;
}
case R.drawable.ic_search: {
if (mDarkTheme)
holder.mTitle.setTextColor(Color.WHITE);
image = mSearchDrawable;
break;
}
case R.drawable.ic_history: {
if (mDarkTheme)
holder.mTitle.setTextColor(Color.WHITE);
image = mHistoryDrawable;
break;
}
default:
if (mDarkTheme)
holder.mTitle.setTextColor(Color.WHITE);
image = mSearchDrawable;
break;
}
holder.mImage.setImageDrawable(image);
return convertView;
}
@Override
public Filter getFilter() {
if (mFilter == null) {
mFilter = new SearchFilter();
}
return mFilter;
}
private static class ClearCacheRunnable implements Runnable {
@NonNull
private final Application app;
public ClearCacheRunnable(@NonNull Application app) {
this.app = app;
}
@Override
public void run() {
File dir = new File(app.getCacheDir().toString());
String[] fileList = dir.list(new NameFilter());
long earliestTimeAllowed = System.currentTimeMillis() - INTERVAL_DAY;
for (String fileName : fileList) {
File file = new File(dir.getPath() + fileName);
if (earliestTimeAllowed > file.lastModified()) {
file.delete();
}
}
}
private static class NameFilter implements FilenameFilter {
@Override
public boolean accept(File dir, @NonNull String filename) {
return filename.endsWith(CACHE_FILE_TYPE);
}
}
}
private class SearchFilter extends Filter {
@NonNull
@Override
protected FilterResults performFiltering(@Nullable CharSequence constraint) {
FilterResults results = new FilterResults();
if (constraint == null) {
return results;
}
String query = constraint.toString().toLowerCase(Locale.getDefault());
if (mUseGoogle && !mIncognito && !mIsExecuting) {
mIsExecuting = true;
new RetrieveSuggestionsTask(query, SuggestionsAdapter.this, BrowserApp.get(mContext)).executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
}
int counter = 0;
synchronized (mBookmarks) {
mBookmarks.clear();
synchronized (SuggestionsAdapter.this) {
for (int n = 0; n < mAllBookmarks.size(); n++) {
if (counter >= 5) {
break;
}
if (mAllBookmarks.get(n).getTitle().toLowerCase(Locale.getDefault())
.startsWith(query)) {
mBookmarks.add(mAllBookmarks.get(n));
counter++;
} else if (mAllBookmarks.get(n).getUrl().contains(query)) {
mBookmarks.add(mAllBookmarks.get(n));
counter++;
}
}
}
}
List<HistoryItem> historyList = mDatabaseHandler.findItemsContaining(constraint.toString());
synchronized (mHistory) {
mHistory.clear();
mHistory.addAll(historyList);
}
results.count = 1;
return results;
}
@Override
public CharSequence convertResultToString(@NonNull Object resultValue) {
return ((HistoryItem) resultValue).getUrl();
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
synchronized (mFilteredList) {
mFilteredList.clear();
List<HistoryItem> filtered = getFilteredList();
Collections.sort(filtered, sComparator);
mFilteredList.addAll(filtered);
}
notifyDataSetChanged();
}
}
@Override
public void resultReceived(@NonNull List<HistoryItem> searchResults) {
mIsExecuting = false;
synchronized (mSuggestions) {
mSuggestions.clear();
mSuggestions.addAll(searchResults);
}
synchronized (mFilteredList) {
mFilteredList.clear();
List<HistoryItem> filtered = getFilteredList();
Collections.sort(filtered, sComparator);
mFilteredList.addAll(filtered);
notifyDataSetChanged();
}
}
@NonNull
private synchronized List<HistoryItem> getFilteredList() {
List<HistoryItem> list = new ArrayList<>(5);
synchronized (mBookmarks) {
synchronized (mHistory) {
synchronized (mSuggestions) {
Iterator<HistoryItem> bookmark = mBookmarks.iterator();
Iterator<HistoryItem> history = mHistory.iterator();
Iterator<HistoryItem> suggestion = mSuggestions.listIterator();
while (list.size() < MAX_SUGGESTIONS) {
if (!bookmark.hasNext() && !suggestion.hasNext() && !history.hasNext()) {
return list;
}
if (bookmark.hasNext()) {
list.add(bookmark.next());
}
if (suggestion.hasNext() && list.size() < MAX_SUGGESTIONS) {
list.add(suggestion.next());
}
if (history.hasNext() && list.size() < MAX_SUGGESTIONS) {
list.add(history.next());
}
}
}
}
}
return list;
}
private static class SuggestionsComparator implements Comparator<HistoryItem> {
@Override
public int compare(@NonNull HistoryItem lhs, @NonNull HistoryItem rhs) {
if (lhs.getImageId() == rhs.getImageId()) return 0;
if (lhs.getImageId() == R.drawable.ic_bookmark) return -1;
if (rhs.getImageId() == R.drawable.ic_bookmark) return 1;
if (lhs.getImageId() == R.drawable.ic_history) return -1;
return 1;
}
}
}
@@ -0,0 +1,21 @@
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<HistoryItem> searchResults);
}
@@ -2,6 +2,8 @@ package acr.browser.lightning.utils;
import android.content.Context;
import android.content.res.AssetManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.io.BufferedReader;
@@ -13,9 +15,14 @@ import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.preference.PreferenceManager;
@Singleton
public class AdBlock {
private static final String TAG = "AdBlock";
@@ -31,34 +38,32 @@ public class AdBlock {
private final Set<String> mBlockedDomainsList = new HashSet<>();
private boolean mBlockAds;
private static final Locale mLocale = Locale.getDefault();
private static AdBlock mInstance;
public static AdBlock getInstance(Context context) {
if (mInstance == null) {
mInstance = new AdBlock(context);
}
return mInstance;
}
@Inject PreferenceManager mPreferenceManager;
private AdBlock(Context context) {
@Inject
public AdBlock(@NonNull Context context) {
BrowserApp.getAppComponent().inject(this);
if (mBlockedDomainsList.isEmpty() && Constants.FULL_VERSION) {
loadHostsFile(context);
}
mBlockAds = PreferenceManager.getInstance().getAdBlockEnabled();
mBlockAds = mPreferenceManager.getAdBlockEnabled();
}
public void updatePreference() {
mBlockAds = PreferenceManager.getInstance().getAdBlockEnabled();
mBlockAds = mPreferenceManager.getAdBlockEnabled();
}
private void loadBlockedDomainsList(final Context context) {
Thread thread = new Thread(new Runnable() {
private void loadBlockedDomainsList(@NonNull final Context context) {
BrowserApp.getTaskThread().execute(new Runnable() {
@Override
public void run() {
AssetManager asset = context.getAssets();
BufferedReader reader = null;
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(
//noinspection IOResourceOpenedButNotSafelyClosed
reader = new BufferedReader(new InputStreamReader(
asset.open(BLOCKED_DOMAINS_LIST_FILE_NAME)));
String line;
while ((line = reader.readLine()) != null) {
@@ -67,19 +72,21 @@ public class AdBlock {
} catch (IOException e) {
Log.wtf(TAG, "Reading blocked domains list from file '"
+ BLOCKED_DOMAINS_LIST_FILE_NAME + "' failed.", e);
} finally {
Utils.close(reader);
}
}
});
thread.start();
}
/**
* 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.
*
* @param url the URL to check for being an ad
* @return true if it is an ad, false if it is not an ad
*/
public boolean isAd(String url) {
public boolean isAd(@Nullable String url) {
if (!mBlockAds || url == null) {
return false;
}
@@ -101,11 +108,13 @@ public class AdBlock {
/**
* Returns the probable domain name for a given URL
*
* @param url the url to parse
* @return returns the domain
* @throws URISyntaxException throws an exception if the string cannot form a URI
*/
private static String getDomainName(String url) throws URISyntaxException {
@NonNull
private static String getDomainName(@NonNull String url) throws URISyntaxException {
int index = url.indexOf('/', 8);
if (index != -1) {
url = url.substring(0, index);
@@ -126,16 +135,18 @@ public class AdBlock {
* simply have a list of hostnames 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
*
* @param context the context needed to read the file
*/
private void loadHostsFile(final Context context) {
Thread thread = new Thread(new Runnable() {
private void loadHostsFile(@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;
@@ -169,6 +180,5 @@ public class AdBlock {
}
}
});
thread.start();
}
}
@@ -0,0 +1,74 @@
package acr.browser.lightning.utils;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.Typeface;
public class DrawableUtils {
public static Bitmap getRoundedNumberImage(int number, int width, int height, int color, int thickness) {
String text;
if (number > 99) {
text = "\u221E";
} else {
text = String.valueOf(number);
}
Bitmap image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(image);
Paint paint = new Paint();
paint.setColor(color);
Typeface boldText = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
paint.setTypeface(boldText);
paint.setTextSize(Utils.dpToPx(14));
paint.setAntiAlias(true);
paint.setTextAlign(Paint.Align.CENTER);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
int radius = Utils.dpToPx(2);
RectF outer = new RectF(0, 0, canvas.getWidth(), canvas.getHeight());
canvas.drawRoundRect(outer, radius, radius, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
radius--;
RectF inner = new RectF(thickness, thickness, canvas.getWidth() - thickness, canvas.getHeight() - thickness);
canvas.drawRoundRect(inner, radius, radius, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
int xPos = (canvas.getWidth() / 2);
int yPos = (int) ((canvas.getHeight() / 2) - ((paint.descent() + paint.ascent()) / 2));
canvas.drawText(String.valueOf(text), xPos, yPos, paint);
return image;
}
public static int mixColor(float fraction, int startValue, int endValue) {
int startInt = startValue;
int startA = (startInt >> 24) & 0xff;
int startR = (startInt >> 16) & 0xff;
int startG = (startInt >> 8) & 0xff;
int startB = startInt & 0xff;
int endInt = endValue;
int endA = (endInt >> 24) & 0xff;
int endR = (endInt >> 16) & 0xff;
int endG = (endInt >> 8) & 0xff;
int endB = endInt & 0xff;
return (startA + (int)(fraction * (endA - startA))) << 24 |
(startR + (int)(fraction * (endR - startR))) << 16 |
(startG + (int)(fraction * (endG - startG))) << 8 |
(startB + (int)(fraction * (endB - startB)));
}
}
@@ -0,0 +1,112 @@
package acr.browser.lightning.utils;
import android.app.Application;
import android.os.Bundle;
import android.os.Parcel;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
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 {
/**
* Writes a bundle to persistent storage in the files directory
* using the specified file name. This method is a blocking
* operation.
*
* @param app the application needed to obtain the file directory.
* @param bundle the bundle to store in persistent storage.
* @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() {
@Override
public void run() {
File outputFile = new File(app.getFilesDir(), name);
FileOutputStream outputStream = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
outputStream = new FileOutputStream(outputFile);
Parcel parcel = Parcel.obtain();
parcel.writeBundle(bundle);
outputStream.write(parcel.marshall());
outputStream.flush();
parcel.recycle();
} catch (IOException e) {
Log.e(Constants.TAG, "Unable to write bundle to storage");
} finally {
Utils.close(outputStream);
}
}
});
}
/**
* Use this method to delete the bundle with the specified name.
* This is a blocking call and should be used within a worker
* thread unless immediate deletion is necessary.
*
* @param app the application object needed to get the file.
* @param name the name of the file.
*/
public static void deleteBundleInStorage(final @NonNull Application app, final @NonNull String name) {
File outputFile = new File(app.getFilesDir(), name);
if (outputFile.exists()) {
outputFile.delete();
}
}
/**
* Reads a bundle from the file with the specified
* name in the peristent storage files directory.
* This method is a blocking operation.
*
* @param app the application needed to obtain the files directory.
* @param name the name of the file to read from.
* @return a valid Bundle loaded using the system class loader
* or null if the method was unable to read the Bundle from storage.
*/
@Nullable
public static Bundle readBundleFromStorage(@NonNull Application app, @NonNull String name) {
File inputFile = new File(app.getFilesDir(), name);
FileInputStream inputStream = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
inputStream = new FileInputStream(inputFile);
Parcel parcel = Parcel.obtain();
byte[] data = new byte[(int) inputStream.getChannel().size()];
//noinspection ResultOfMethodCallIgnored
inputStream.read(data, 0, data.length);
parcel.unmarshall(data, 0, data.length);
parcel.setDataPosition(0);
Bundle out = parcel.readBundle(ClassLoader.getSystemClassLoader());
out.putAll(out);
parcel.recycle();
return out;
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "Unable to read bundle from storage");
} catch (IOException e) {
e.printStackTrace();
} finally {
//noinspection ResultOfMethodCallIgnored
inputFile.delete();
Utils.close(inputStream);
}
return null;
}
}
@@ -7,6 +7,9 @@ import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.webkit.WebView;
@@ -15,7 +18,7 @@ import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import acr.browser.lightning.controller.BrowserController;
import acr.browser.lightning.constant.Constants;
public class IntentUtils {
@@ -25,14 +28,14 @@ public class IntentUtils {
+ // switch on case insensitive matching
'('
+ // begin group for schema
"(?:http|https|file):\\/\\/" + "|(?:inline|data|about|javascript):" + "|(?:.*:.*@)"
"(?:http|https|file)://" + "|(?:inline|data|about|javascript):" + "|(?:.*:.*@)"
+ ')' + "(.*)");
public IntentUtils(BrowserController controller) {
mActivity = controller.getActivity();
public IntentUtils(Activity activity) {
mActivity = activity;
}
public boolean startActivityForUrl(WebView tab, String url) {
public boolean startActivityForUrl(@Nullable WebView tab, @NonNull String url) {
Intent intent;
try {
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
@@ -41,6 +44,12 @@ public class IntentUtils {
return false;
}
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
intent.setSelector(null);
}
if (mActivity.getPackageManager().resolveActivity(intent, 0) == null) {
String packagename = intent.getPackage();
if (packagename != null) {
@@ -53,10 +62,8 @@ public class IntentUtils {
return false;
}
}
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
if (tab != null) {
intent.putExtra(mActivity.getPackageName() + ".Origin", 1);
intent.putExtra(Constants.INTENT_ORIGIN, 1);
}
Matcher m = ACCEPTED_URI_SCHEMA.matcher(url);
@@ -0,0 +1,67 @@
package acr.browser.lightning.utils;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewTreeObserver;
public class KeyboardHelper {
public 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;
}
});
}
}
@@ -0,0 +1,80 @@
package acr.browser.lightning.utils;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.inputmethod.InputMethodManager;
import java.lang.reflect.Method;
public class MemoryLeakUtils {
private static final String TAG = MemoryLeakUtils.class.getSimpleName();
private static Method sFinishInputLocked = null;
/**
* Clears the mNextServedView and mServedView in
* InputMethodManager and keeps them from leaking.
*
* @param application the application needed to get
* the InputMethodManager that is
* leaking the views.
*/
public static void clearNextServedView(@NonNull Application application) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
// This shouldn't be a problem on N
return;
}
InputMethodManager imm = (InputMethodManager) application.getSystemService(Context.INPUT_METHOD_SERVICE);
if (sFinishInputLocked == null) {
try {
sFinishInputLocked = InputMethodManager.class.getDeclaredMethod("finishInputLocked");
} catch (NoSuchMethodException e) {
Log.d(TAG, "Unable to find method in clearNextServedView", e);
}
}
if (sFinishInputLocked != null) {
sFinishInputLocked.setAccessible(true);
try {
sFinishInputLocked.invoke(imm);
} catch (Exception e) {
Log.d(TAG, "Unable to invoke method in clearNextServedView", e);
}
}
}
public static abstract class LifecycleAdapter implements Application.ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
@Override
public void onActivityStarted(Activity activity) {}
@Override
public void onActivityResumed(Activity activity) {}
@Override
public void onActivityPaused(Activity activity) {}
@Override
public void onActivityStopped(Activity activity) {}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
@Override
public void onActivityDestroyed(Activity activity) {}
}
}
@@ -1,75 +0,0 @@
package acr.browser.lightning.utils;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.annotation.NonNull;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Copyright 8/22/2015 Anthony Restaino
*/
public class PermissionsManager {
private static PermissionsManager mInstance;
private Set<String> mPendingRequests = new HashSet<>();
public static PermissionsManager getInstance() {
if (mInstance == null) {
mInstance = new PermissionsManager();
}
return mInstance;
}
public void requestPermissionsIfNecessary(Activity activity, @NonNull String[] permissions) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || activity == null) {
return;
}
List<String> permList = new ArrayList<>();
for (String perm : permissions) {
if (activity.checkSelfPermission(perm) != PackageManager.PERMISSION_GRANTED
&& !mPendingRequests.contains(perm)) {
permList.add(perm);
}
}
if (!permList.isEmpty()) {
String[] permsToRequest = permList.toArray(new String[permList.size()]);
mPendingRequests.addAll(permList);
activity.requestPermissions(permsToRequest, 1);
}
}
public static boolean checkPermission(Activity activity, @NonNull String permission) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
} else if (activity == null) {
return false;
}
return activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
}
public static boolean checkPermissions(Activity activity, @NonNull String[] permissions) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
} else if (activity == null) {
return false;
}
boolean permissionsNecessary = true;
for (String perm : permissions) {
permissionsNecessary &= activity.checkSelfPermission(perm) == PackageManager.PERMISSION_GRANTED;
}
return permissionsNecessary;
}
public void notifyPermissionsChange(String[] permissions) {
for (String perm : permissions) {
mPendingRequests.remove(perm);
}
}
}
@@ -0,0 +1,16 @@
package acr.browser.lightning.utils;
public class Preconditions {
/**
* Ensure that an object is not null
* and throw a RuntimeException if it
* is null.
*
* @param object check nullness on this object.
*/
public static void checkNonNull(Object object) {
if (object == null) {
throw new RuntimeException("Object must not be null");
}
}
}
@@ -1,49 +1,46 @@
package acr.browser.lightning.utils;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.support.annotation.NonNull;
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;
import javax.inject.Singleton;
import acr.browser.lightning.R;
import acr.browser.lightning.activity.BrowserApp;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.bus.BrowserEvents;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.preference.PreferenceManager;
import info.guardianproject.netcipher.proxy.OrbotHelper;
import info.guardianproject.netcipher.web.WebkitProxy;
/**
* 6/4/2015 Anthony Restaino
*/
@Singleton
public class ProxyUtils {
// Helper
private I2PAndroidHelper mI2PHelper;
private static boolean mI2PHelperBound;
private static boolean mI2PProxyInitialized;
private PreferenceManager mPreferences;
private static ProxyUtils mInstance;
private ProxyUtils(Context context) {
mPreferences = PreferenceManager.getInstance();
mI2PHelper = new I2PAndroidHelper(context);
}
@Inject PreferenceManager mPreferences;
@Inject I2PAndroidHelper mI2PHelper;
@Inject Bus mBus;
public static ProxyUtils getInstance(@NonNull Context context) {
if (mInstance == null) {
mInstance = new ProxyUtils(context);
}
return mInstance;
@Inject
public ProxyUtils() {
BrowserApp.getAppComponent().inject(this);
}
/*
* If Orbot/Tor or I2P is installed, prompt the user if they want to enable
* proxying for this session
*/
public void checkForProxy(final Activity activity) {
public void checkForProxy(@NonNull final Activity activity) {
boolean useProxy = mPreferences.getUseProxy();
final boolean orbotInstalled = OrbotHelper.isOrbotInstalled(activity);
@@ -106,7 +103,7 @@ public class ProxyUtils {
/*
* Initialize WebKit Proxying
*/
private void initializeProxy(Activity activity) {
private void initializeProxy(@NonNull Activity activity) {
String host;
int port;
@@ -144,13 +141,13 @@ public class ProxyUtils {
}
public boolean isProxyReady(Activity activity) {
public boolean isProxyReady() {
if (mPreferences.getProxyChoice() == Constants.PROXY_I2P) {
if (!mI2PHelper.isI2PAndroidRunning()) {
Utils.showSnackbar(activity, R.string.i2p_not_running);
mBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.i2p_not_running));
return false;
} else if (!mI2PHelper.areTunnelsActive()) {
Utils.showSnackbar(activity, R.string.i2p_tunnels_not_ready);
mBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.i2p_tunnels_not_ready));
return false;
}
}
@@ -158,7 +155,7 @@ public class ProxyUtils {
return true;
}
public void updateProxySettings(Activity activity) {
public void updateProxySettings(@NonNull Activity activity) {
if (mPreferences.getUseProxy()) {
initializeProxy(activity);
} else {
@@ -191,7 +188,7 @@ public class ProxyUtils {
}
}
public int setProxyChoice(int choice, Activity activity) {
public static int setProxyChoice(int choice, @NonNull Activity activity) {
switch (choice) {
case Constants.PROXY_ORBOT:
if (!OrbotHelper.isOrbotInstalled(activity)) {
@@ -201,7 +198,7 @@ public class ProxyUtils {
break;
case Constants.PROXY_I2P:
I2PAndroidHelper ih = new I2PAndroidHelper(activity.getApplicationContext());
I2PAndroidHelper ih = new I2PAndroidHelper(BrowserApp.get(activity));
if (!ih.isI2PAndroidInstalled()) {
choice = Constants.NO_PROXY;
ih.promptToInstall(activity);
@@ -1,22 +1,22 @@
package acr.browser.lightning.utils;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
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;
import android.support.annotation.ColorInt;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.TypedValue;
import android.widget.ImageView;
@@ -45,26 +45,28 @@ public class ThemeUtils {
return color;
}
@ColorInt
public static int getIconLightThemeColor(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return context.getResources().getColor(R.color.icon_light_theme, context.getTheme());
}
return context.getResources().getColor(R.color.icon_light_theme);
return ContextCompat.getColor(context, R.color.icon_light_theme);
}
@ColorInt
public static int getIconDarkThemeColor(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return context.getResources().getColor(R.color.icon_dark_theme, context.getTheme());
}
return context.getResources().getColor(R.color.icon_dark_theme);
return ContextCompat.getColor(context, R.color.icon_dark_theme);
}
public static void themeImageView(ImageView icon, Context context, boolean dark) {
@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);
}
public static Bitmap getThemedBitmap(Context context, @DrawableRes int res, boolean dark) {
@NonNull
public static Bitmap getThemedBitmap(@NonNull Context context, @DrawableRes int res, boolean dark) {
int color = dark ? getIconDarkThemeColor(context) : getIconLightThemeColor(context);
Bitmap sourceBitmap = BitmapFactory.decodeResource(context.getResources(), res);
Bitmap resultBitmap = Bitmap.createBitmap(sourceBitmap.getWidth(), sourceBitmap.getHeight(), Bitmap.Config.ARGB_8888);
@@ -77,47 +79,27 @@ public class ThemeUtils {
return resultBitmap;
}
@Nullable
@NonNull
public static Drawable getThemedDrawable(@NonNull Context context, @DrawableRes int res, boolean dark) {
int color = dark ? getIconDarkThemeColor(context) : getIconLightThemeColor(context);
final Drawable drawable;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
drawable = context.getResources().getDrawable(res);
} else {
drawable = context.getDrawable(res);
}
if (drawable == null)
return null;
final Drawable drawable = ContextCompat.getDrawable(context, res);
drawable.mutate();
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
return drawable;
}
@Nullable
public static Drawable getLightThemedDrawable(@NonNull Context context, @DrawableRes int res) {
final Drawable drawable;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
drawable = context.getResources().getDrawable(res);
} else {
drawable = context.getDrawable(res);
}
if (drawable == null)
return null;
drawable.mutate();
drawable.setColorFilter(getIconLightThemeColor(context), PorterDuff.Mode.SRC_IN);
return drawable;
}
@NonNull
public static ColorDrawable getSelectedBackground(@NonNull Context context, boolean dark) {
Resources res = context.getResources();
int color;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
color = (dark) ? res.getColor(R.color.divider_dark, context.getTheme()) :
res.getColor(R.color.divider_light, context.getTheme());
} else {
color = (dark) ? res.getColor(R.color.divider_dark) :
res.getColor(R.color.divider_light);
}
@ColorInt final int color = (dark) ? ContextCompat.getColor(context, R.color.selected_dark) :
ContextCompat.getColor(context, R.color.selected_light);
return new ColorDrawable(color);
}
public static int getThemedTextHintColor(boolean dark){
return 0x80ffffff & (dark ? Color.WHITE : Color.BLACK);
}
public static int getTextColor(@NonNull Context context) {
return getColor(context, android.R.attr.editTextColor);
}
}
@@ -15,24 +15,30 @@
*/
package acr.browser.lightning.utils;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Patterns;
import android.webkit.URLUtil;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import acr.browser.lightning.constant.BookmarkPage;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.constant.HistoryPage;
import acr.browser.lightning.constant.StartPage;
/**
* Utility methods for Url manipulation
*/
public class UrlUtils {
static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile(
private static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile(
"(?i)" + // switch on case insensitive matching
"(" + // begin group for schema
"(?:http|https|file):\\/\\/" +
'(' + // begin group for schema
"(?:http|https|file)://" +
"|(?:inline|data|about|javascript):" +
"|(?:.*:.*@)" +
")" +
')' +
"(.*)");
// Google search
public final static String QUERY_PLACE_HOLDER = "%s";
@@ -54,7 +60,8 @@ public class UrlUtils {
* @return a stripped url like "www.google.com", or the original string if it could
* not be stripped
*/
public static String stripUrl(String url) {
@Nullable
public static String stripUrl(@Nullable String url) {
if (url == null) return null;
Matcher m = STRIP_URL_PATTERN.matcher(url);
if (m.matches()) {
@@ -75,7 +82,8 @@ public class UrlUtils {
* URL. If false, invalid URLs will return null
* @return Original or modified URL
*/
public static String smartUrlFilter(String url, boolean canBeSearch, String searchUrl) {
@NonNull
public static String smartUrlFilter(@NonNull String url, boolean canBeSearch, String searchUrl) {
String inUrl = url.trim();
boolean hasSpace = inUrl.indexOf(' ') != -1;
Matcher matcher = ACCEPTED_URI_SCHEMA.matcher(inUrl);
@@ -100,11 +108,12 @@ public class UrlUtils {
return URLUtil.composeSearchUrl(inUrl,
searchUrl, QUERY_PLACE_HOLDER);
}
return null;
return "";
}
/* package */
static String fixUrl(String inUrl) {
@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
@@ -136,7 +145,8 @@ public class UrlUtils {
// Returns the filtered URL. Cannot return null, but can return an empty string
/* package */
static String filteredUrl(String inUrl) {
@Nullable
static String filteredUrl(@Nullable String inUrl) {
if (inUrl == null) {
return "";
}
@@ -146,4 +156,44 @@ public class UrlUtils {
}
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));
}
/**
* 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 isBookmarkUrl(@Nullable String url) {
return url != null && url.startsWith(Constants.FILE) && url.endsWith(BookmarkPage.FILENAME);
}
/**
* Determines if the url is a url for the history page.
*
* @param url the url to check, may be null.
* @return true if the url is a history url, false otherwise.
*/
public static boolean isHistoryUrl(@Nullable String url) {
return url != null && url.startsWith(Constants.FILE) && url.endsWith(HistoryPage.FILENAME);
}
/**
* Determines if the url is a url for the start page.
*
* @param url the url to check, may be null.
* @return true if the url is a start page url, false otherwise.
*/
public static boolean isStartPageUrl(@Nullable String url) {
return url != null && url.startsWith(Constants.FILE) && url.endsWith(StartPage.FILENAME);
}
}
@@ -3,6 +3,7 @@
*/
package acr.browser.lightning.utils;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
@@ -11,6 +12,7 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -18,19 +20,23 @@ import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.webkit.URLUtil;
import com.anthonycr.grant.PermissionsManager;
import com.anthonycr.grant.PermissionsResultAction;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
@@ -40,19 +46,59 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import acr.browser.lightning.R;
import acr.browser.lightning.activity.MainActivity;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.database.HistoryItem;
import acr.browser.lightning.download.DownloadHandler;
import acr.browser.lightning.preference.PreferenceManager;
public final class Utils {
public static void downloadFile(final Activity activity, final String url,
final String userAgent, final String contentDisposition) {
String fileName = URLUtil.guessFileName(url, null, null);
DownloadHandler.onDownloadStart(activity, url, userAgent, contentDisposition, null
);
Log.i(Constants.TAG, "Downloading" + fileName);
public static boolean doesSupportHeaders() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
}
/**
* Downloads a file from the specified URL. Handles permissions
* requests, and creates all the necessary dialogs that must be
* showed to the user.
*
* @param activity activity needed to created dialogs.
* @param url url to download from.
* @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,
final String userAgent, final String contentDisposition) {
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(activity, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE}, new PermissionsResultAction() {
@Override
public void onGranted() {
String fileName = URLUtil.guessFileName(url, null, null);
DownloadHandler.onDownloadStart(activity, manager, url, userAgent, contentDisposition, null);
Log.i(Constants.TAG, "Downloading" + fileName);
}
@Override
public void onDenied(String permission) {
// TODO Show Message
}
});
}
/**
* Creates a new intent that can launch the email
* app with a subject, address, body, and cc. It
* is used to handle mail:to links.
*
* @param address the address to send the email to.
* @param subject the subject of the email.
* @param body the body of the email.
* @param cc extra addresses to CC.
* @return a valid intent.
*/
@NonNull
public static Intent newEmailIntent(String address, String subject,
String body, String cc) {
Intent intent = new Intent(Intent.ACTION_SEND);
@@ -64,12 +110,19 @@ public final class Utils {
return intent;
}
public static void createInformativeDialog(Context context, @StringRes int title, @StringRes int message) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
/**
* Creates a dialog with only a title, message, and okay button.
*
* @param activity the activity needed to create a dialog.
* @param title the title of the dialog.
* @param message the message of the dialog.
*/
public static void createInformativeDialog(@NonNull Activity activity, @StringRes int title, @StringRes int message) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(title);
builder.setMessage(message)
.setCancelable(true)
.setPositiveButton(context.getResources().getString(R.string.action_ok),
.setPositiveButton(activity.getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
@@ -79,30 +132,51 @@ public final class Utils {
alert.show();
}
/**
* Displays a snackbar to the user with a String resource.
*
* @param activity the activity needed to create a snackbar.
* @param resource the string resource to show to the user.
*/
public static void showSnackbar(@NonNull Activity activity, @StringRes int resource) {
View view = activity.findViewById(android.R.id.content);
if (view == null) return;
Snackbar.make(view, resource, Snackbar.LENGTH_SHORT).show();
}
public static void showSnackbar(@NonNull Activity activity, String message) {
/**
* Displays a snackbar to the user with a string message.
*
* @param activity the activity needed to create a snackbar.
* @param message the string message to show to the user.
*/
public static void showSnackbar(@NonNull Activity activity, @NonNull String message) {
View view = activity.findViewById(android.R.id.content);
if (view == null) return;
Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
}
/**
* Converts Density Pixels (DP) to Pixels (PX)
* Converts Density Pixels (DP) to Pixels (PX).
*
* @param dp the number of density pixels to convert
* @return the number of pixels
* @param dp the number of density pixels to convert.
* @return the number of pixels that the conversion generates.
*/
public static int dpToPx(int dp) {
public static int dpToPx(float dp) {
DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
return (int) (dp * metrics.density + 0.5f);
}
public static String getDomainName(String url) {
/**
* Extracts the domain name from a URL.
*
* @param url the URL to extract the domain from.
* @return the domain name, or the URL if the domain
* could not be extracted. The domain name may include
* HTTPS if the URL is an SSL supported URL.
*/
@Nullable
public static String getDomainName(@Nullable String url) {
if (url == null || url.isEmpty()) return "";
boolean ssl = url.startsWith(Constants.HTTPS);
@@ -130,16 +204,11 @@ public final class Utils {
return domain.startsWith("www.") ? domain.substring(4) : domain;
}
public static String getProtocol(String url) {
int index = url.indexOf('/');
return url.substring(0, index + 2);
}
public static String[] getArray(String input) {
public static String[] getArray(@NonNull String input) {
return input.split(Constants.SEPARATOR);
}
public static void trimCache(Context context) {
public static void trimCache(@NonNull Context context) {
try {
File dir = context.getCacheDir();
@@ -151,7 +220,7 @@ public final class Utils {
}
}
public static boolean deleteDir(File dir) {
private static boolean deleteDir(@Nullable File dir) {
if (dir != null && dir.isDirectory()) {
String[] children = dir.list();
for (String aChildren : children) {
@@ -172,7 +241,7 @@ public final class Utils {
* @param bitmap is the bitmap to pad.
* @return the padded bitmap.
*/
public static Bitmap padFavicon(Bitmap bitmap) {
public static Bitmap padFavicon(@NonNull Bitmap bitmap) {
int padding = Utils.dpToPx(4);
Bitmap paddedBitmap = Bitmap.createBitmap(bitmap.getWidth() + padding, bitmap.getHeight()
@@ -233,7 +302,7 @@ public final class Utils {
* @param context the context needed to obtain the PackageManager
* @return true if flash is installed, false otherwise
*/
public static boolean isFlashInstalled(Context context) {
public static boolean isFlashInstalled(@NonNull Context context) {
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo ai = pm.getApplicationInfo("com.adobe.flashplayer", 0);
@@ -252,7 +321,7 @@ public final class Utils {
*
* @param closeable the object to close
*/
public static void close(Closeable closeable) {
public static void close(@Nullable Closeable closeable) {
if (closeable == null)
return;
try {
@@ -262,11 +331,21 @@ public final class Utils {
}
}
public static Drawable getDrawable(Context context, @DrawableRes int res) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return context.getDrawable(res);
} else {
return context.getResources().getDrawable(res);
/**
* Utility method to close cursors. Cursor did not
* implement Closeable until API 16, so using this
* method for when we want to close a cursor.
*
* @param cursor the cursor to close
*/
public static void close(@Nullable Cursor cursor) {
if (cursor == null) {
return;
}
try {
cursor.close();
} catch (Exception e) {
e.printStackTrace();
}
}
@@ -277,7 +356,7 @@ public final class Utils {
* @param canvas the canvas to draw upon
* @param color the color to use to draw the tab
*/
public static void drawTrapezoid(Canvas canvas, int color, boolean withShader) {
public static void drawTrapezoid(@NonNull Canvas canvas, int color, boolean withShader) {
Paint paint = new Paint();
paint.setColor(color);
@@ -309,4 +388,33 @@ public final class Utils {
canvas.drawPath(wallpath, paint);
}
/**
* Creates a shortcut on the homescreen using the
* {@link HistoryItem} information that opens the
* browser. The icon, URL, and title are used in
* the creation of the shortcut.
*
* @param activity the activity needed to create
* the intent and show a snackbar message
* @param item the HistoryItem to create the shortcut from
*/
public static void createShortcut(@NonNull Activity activity, @NonNull HistoryItem item) {
if (TextUtils.isEmpty(item.getUrl())) {
return;
}
Log.d(Constants.TAG, "Creating shortcut: " + item.getTitle() + ' ' + item.getUrl());
Intent shortcutIntent = new Intent(activity, MainActivity.class);
shortcutIntent.setData(Uri.parse(item.getUrl()));
final String title = TextUtils.isEmpty(item.getTitle()) ? activity.getString(R.string.untitled) : item.getTitle();
Intent addIntent = new Intent();
addIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, title);
addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, item.getBitmap());
addIntent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
activity.sendBroadcast(addIntent);
Utils.showSnackbar(activity, R.string.message_added_to_homescreen);
}
}
@@ -2,8 +2,8 @@ package acr.browser.lightning.utils;
import android.content.Context;
import android.os.Build;
import android.provider.Browser;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.WebIconDatabase;
@@ -23,7 +23,9 @@ public class WebUtils {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
c.removeAllCookies(null);
} else {
//noinspection deprecation
CookieSyncManager.createInstance(context);
//noinspection deprecation
c.removeAllCookie();
}
}
@@ -32,19 +34,21 @@ public class WebUtils {
WebStorage.getInstance().deleteAllData();
}
public static void clearHistory(@NonNull Context context) {
HistoryDatabase.getInstance(context).deleteHistory();
public static void clearHistory(@NonNull Context context, @NonNull HistoryDatabase historyDatabase) {
historyDatabase.deleteHistory();
WebViewDatabase m = WebViewDatabase.getInstance(context);
m.clearFormData();
m.clearHttpAuthUsernamePassword();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
//noinspection deprecation
m.clearUsernamePassword();
//noinspection deprecation
WebIconDatabase.getInstance().removeAllIcons();
}
Utils.trimCache(context);
}
public static void clearCache(WebView view) {
public static void clearCache(@Nullable WebView view) {
if (view == null) return;
view.clearCache(true);
}
@@ -8,6 +8,7 @@ import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.animation.Animation;
@@ -39,12 +40,12 @@ public class AnimatedProgressBar extends LinearLayout {
private int mDrawWidth = 0;
private int mProgressColor;
public AnimatedProgressBar(Context context, AttributeSet attrs) {
public AnimatedProgressBar(@NonNull Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public AnimatedProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
public AnimatedProgressBar(@NonNull Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
@@ -55,7 +56,7 @@ public class AnimatedProgressBar extends LinearLayout {
* @param context is the context passed by the constructor
* @param attrs is the attribute set passed by the constructor
*/
private void init(final Context context, AttributeSet attrs) {
private void init(@NonNull final Context context, AttributeSet attrs) {
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AnimatedProgressBar, 0, 0);
int backgroundColor;
try { // Retrieve the style of the progress bar that the user hopefully set
@@ -90,7 +91,7 @@ public class AnimatedProgressBar extends LinearLayout {
private final Rect mRect = new Rect();
@Override
protected void onDraw(Canvas canvas) {
protected void onDraw(@NonNull Canvas canvas) {
mPaint.setColor(mProgressColor);
mPaint.setStrokeWidth(10);
mRect.right = mRect.left + mDrawWidth;
@@ -209,6 +210,7 @@ public class AnimatedProgressBar extends LinearLayout {
super.onRestoreInstanceState(state);
}
@NonNull
@Override
protected Parcelable onSaveInstanceState() {
@@ -0,0 +1,42 @@
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);
}
}
}
@@ -0,0 +1,208 @@
package acr.browser.lightning.view;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import android.webkit.GeolocationPermissions;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import com.anthonycr.grant.PermissionsManager;
import com.anthonycr.grant.PermissionsResultAction;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.controller.UIController;
import acr.browser.lightning.utils.Preconditions;
class LightningChromeClient extends WebChromeClient {
private static final String TAG = LightningChromeClient.class.getSimpleName();
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;
LightningChromeClient(@NonNull Activity activity, @NonNull LightningView lightningView) {
Preconditions.checkNonNull(activity);
Preconditions.checkNonNull(lightningView);
mActivity = activity;
mUIController = (UIController) activity;
mLightningView = lightningView;
}
@Override
public void onProgressChanged(WebView view, int newProgress) {
if (mLightningView.isShown()) {
mUIController.updateProgress(newProgress);
}
}
@Override
public void onReceivedIcon(@NonNull WebView view, Bitmap icon) {
mLightningView.getTitleInfo().setFavicon(icon);
mUIController.tabChanged(mLightningView);
cacheFavicon(view.getUrl(), icon, mActivity);
}
/**
* Naive caching of the favicon according to the domain name of the URL
*
* @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);
if (uri.getHost() == null) {
return;
}
BrowserApp.getIOThread().execute(new IconCacheTask(uri, icon, BrowserApp.get(context)));
}
@Override
public void onReceivedTitle(@Nullable WebView view, @Nullable String title) {
if (title != null && !title.isEmpty()) {
mLightningView.getTitleInfo().setTitle(title);
} else {
mLightningView.getTitleInfo().setTitle(mActivity.getString(R.string.untitled));
}
mUIController.tabChanged(mLightningView);
if (view != null && view.getUrl() != null) {
mUIController.updateHistory(title, view.getUrl());
}
}
@Override
public void onGeolocationPermissionsShowPrompt(@NonNull final String origin,
@NonNull final GeolocationPermissions.Callback callback) {
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(mActivity, PERMISSIONS, new PermissionsResultAction() {
@Override
public void onGranted() {
final boolean remember = true;
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle(mActivity.getString(R.string.location));
String org;
if (origin.length() > 50) {
org = origin.subSequence(0, 50) + "...";
} else {
org = origin;
}
builder.setMessage(org + mActivity.getString(R.string.message_location))
.setCancelable(true)
.setPositiveButton(mActivity.getString(R.string.action_allow),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
callback.invoke(origin, true, remember);
}
})
.setNegativeButton(mActivity.getString(R.string.action_dont_allow),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
callback.invoke(origin, false, remember);
}
});
AlertDialog alert = builder.create();
alert.show();
}
@Override
public void onDenied(String permission) {
//TODO show message and/or turn off setting
}
});
}
@Override
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture,
Message resultMsg) {
mUIController.onCreateWindow(resultMsg);
return true;
}
@Override
public void onCloseWindow(WebView window) {
mUIController.onCloseWindow(mLightningView);
}
@SuppressWarnings("unused")
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
mUIController.openFileChooser(uploadMsg);
}
@SuppressWarnings("unused")
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
mUIController.openFileChooser(uploadMsg);
}
@SuppressWarnings("unused")
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
mUIController.openFileChooser(uploadMsg);
}
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
WebChromeClient.FileChooserParams fileChooserParams) {
mUIController.showFileChooser(filePathCallback);
return true;
}
/**
* Obtain an image that is displayed as a placeholder on a video until the video has initialized
* and can begin loading.
*
* @return a Bitmap that can be used as a place holder for videos.
*/
@Nullable
@Override
public Bitmap getDefaultVideoPoster() {
final Resources resources = mActivity.getResources();
return BitmapFactory.decodeResource(resources, android.R.drawable.spinner_background);
}
/**
* Inflate a view to send to a LightningView when it needs to display a video and has to
* show a loading dialog. Inflates a progress view and returns it.
*
* @return A view that should be used to display the state
* of a video's loading progress.
*/
@Override
public View getVideoLoadingProgressView() {
LayoutInflater inflater = LayoutInflater.from(mActivity);
return inflater.inflate(R.layout.video_loading_progress, null);
}
@Override
public void onHideCustomView() {
mUIController.onHideCustomView();
}
@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
mUIController.onShowCustomView(view, callback);
}
@SuppressWarnings("deprecation")
@Override
public void onShowCustomView(View view, int requestedOrientation,
CustomViewCallback callback) {
mUIController.onShowCustomView(view, callback, requestedOrientation);
}
}
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
@@ -0,0 +1,106 @@
package acr.browser.lightning.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import acr.browser.lightning.R;
import acr.browser.lightning.utils.ThemeUtils;
import acr.browser.lightning.utils.Utils;
/**
* {@link LightningViewTitle} acts as a container class
* for the favicon and page title, the information used
* by the tab adapters to show the tabs to the user.
*/
class LightningViewTitle {
@Nullable private static Bitmap DEFAULT_DARK_ICON;
@Nullable private static Bitmap DEFAULT_LIGHT_ICON;
@Nullable private Bitmap mFavicon = null;
@NonNull private String mTitle;
@NonNull private final Context mContext;
public LightningViewTitle(@NonNull Context context) {
mContext = context;
mTitle = context.getString(R.string.action_new_tab);
}
/**
* Set the current favicon to a new Bitmap.
* May be null, if null, the default will be used.
*
* @param favicon the potentially null favicon to set.
*/
public void setFavicon(@Nullable Bitmap favicon) {
if (favicon == null) {
mFavicon = null;
} else {
mFavicon = Utils.padFavicon(favicon);
}
}
/**
* Helper method to initialize the DEFAULT_ICON variables
*
* @param context the context needed to initialize the Bitmap.
* @param darkTheme whether the icon should be themed dark or not.
* @return a not null icon.
*/
@NonNull
private static Bitmap getDefaultIcon(@NonNull Context context, boolean darkTheme) {
if (darkTheme) {
if (DEFAULT_DARK_ICON == null) {
DEFAULT_DARK_ICON = ThemeUtils.getThemedBitmap(context, R.drawable.ic_webpage, true);
}
return DEFAULT_DARK_ICON;
} else {
if (DEFAULT_LIGHT_ICON == null) {
DEFAULT_LIGHT_ICON = ThemeUtils.getThemedBitmap(context, R.drawable.ic_webpage, false);
}
return DEFAULT_LIGHT_ICON;
}
}
/**
* Set the current title to a new title.
* Must not be null.
*
* @param title the non-null title to set.
*/
public void setTitle(@Nullable String title) {
if (title == null) {
mTitle = "";
} else {
mTitle = title;
}
}
/**
* Gets the current title, which is not null.
* Can be an empty string.
*
* @return the non-null title.
*/
@NonNull
public String getTitle() {
return mTitle;
}
/**
* Gets the favicon of the page, which is not null.
* Either the favicon, or a default icon.
*
* @return the favicon or a default if that is null.
*/
@NonNull
public Bitmap getFavicon(boolean darkTheme) {
if (mFavicon == null) {
return getDefaultIcon(mContext, darkTheme);
}
return mFavicon;
}
}
@@ -0,0 +1,325 @@
package acr.browser.lightning.view;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.MailTo;
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.v7.app.AlertDialog;
import android.text.InputType;
import android.text.method.PasswordTransformationMethod;
import android.util.Log;
import android.webkit.HttpAuthHandler;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.EditText;
import android.widget.LinearLayout;
import java.io.ByteArrayInputStream;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.controller.UIController;
import acr.browser.lightning.utils.AdBlock;
import acr.browser.lightning.utils.IntentUtils;
import acr.browser.lightning.utils.Preconditions;
import acr.browser.lightning.utils.ProxyUtils;
import acr.browser.lightning.utils.Utils;
public class LightningWebClient extends WebViewClient {
@NonNull private final Activity mActivity;
@NonNull private final LightningView mLightningView;
@NonNull private final UIController mUIController;
@NonNull private final IntentUtils mIntentUtils;
@Inject ProxyUtils mProxyUtils;
@Inject AdBlock mAdBlock;
LightningWebClient(@NonNull Activity activity, @NonNull LightningView lightningView) {
BrowserApp.getAppComponent().inject(this);
Preconditions.checkNonNull(activity);
Preconditions.checkNonNull(lightningView);
mActivity = activity;
mUIController = (UIController) activity;
mLightningView = lightningView;
mAdBlock.updatePreference();
mIntentUtils = new IntentUtils(activity);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, @NonNull WebResourceRequest request) {
if (mAdBlock.isAd(request.getUrl().toString())) {
ByteArrayInputStream EMPTY = new ByteArrayInputStream("".getBytes());
return new WebResourceResponse("text/plain", "utf-8", EMPTY);
}
return super.shouldInterceptRequest(view, request);
}
@Nullable
@SuppressWarnings("deprecation")
@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (mAdBlock.isAd(url)) {
ByteArrayInputStream EMPTY = new ByteArrayInputStream("".getBytes());
return new WebResourceResponse("text/plain", "utf-8", EMPTY);
}
return null;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void onPageFinished(@NonNull WebView view, String url) {
if (view.isShown()) {
mUIController.updateUrl(url, true);
mUIController.setBackButtonEnabled(view.canGoBack());
mUIController.setForwardButtonEnabled(view.canGoForward());
view.postInvalidate();
}
if (view.getTitle() == null || view.getTitle().isEmpty()) {
mLightningView.getTitleInfo().setTitle(mActivity.getString(R.string.untitled));
} else {
mLightningView.getTitleInfo().setTitle(view.getTitle());
}
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT &&
mLightningView.getInvertePage()) {
view.evaluateJavascript(Constants.JAVASCRIPT_INVERT_PAGE, null);
}
mUIController.tabChanged(mLightningView);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
mLightningView.getTitleInfo().setFavicon(null);
if (mLightningView.isShown()) {
mUIController.updateUrl(url, false);
mUIController.showActionBar();
}
mUIController.tabChanged(mLightningView);
}
@Override
public void onReceivedHttpAuthRequest(final WebView view, @NonNull final HttpAuthHandler handler,
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),
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");
}
})
.setNegativeButton(mActivity.getString(R.string.action_cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
handler.cancel();
}
});
AlertDialog alert = builder.create();
alert.show();
}
private boolean mIsRunning = false;
private float mZoomScale = 0.0f;
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void onScaleChanged(@NonNull final WebView view, final float oldScale, final float newScale) {
if (view.isShown() && mLightningView.mPreferences.getTextReflowEnabled() &&
Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
if (mIsRunning)
return;
if (Math.abs(mZoomScale - newScale) > 0.01f) {
mIsRunning = view.postDelayed(new Runnable() {
@Override
public void run() {
mZoomScale = newScale;
view.evaluateJavascript(Constants.JAVASCRIPT_TEXT_REFLOW, null);
mIsRunning = false;
}
}, 100);
}
}
}
@NonNull
private static List<Integer> getAllSslErrorMessageCodes(@NonNull SslError error) {
List<Integer> errorCodeMessageCodes = new ArrayList<>(1);
if (error.hasError(SslError.SSL_DATE_INVALID)) {
errorCodeMessageCodes.add(R.string.message_certificate_date_invalid);
}
if (error.hasError(SslError.SSL_EXPIRED)) {
errorCodeMessageCodes.add(R.string.message_certificate_expired);
}
if (error.hasError(SslError.SSL_IDMISMATCH)) {
errorCodeMessageCodes.add(R.string.message_certificate_domain_mismatch);
}
if (error.hasError(SslError.SSL_NOTYETVALID)) {
errorCodeMessageCodes.add(R.string.message_certificate_not_yet_valid);
}
if (error.hasError(SslError.SSL_UNTRUSTED)) {
errorCodeMessageCodes.add(R.string.message_certificate_untrusted);
}
if (error.hasError(SslError.SSL_INVALID)) {
errorCodeMessageCodes.add(R.string.message_certificate_invalid);
}
return errorCodeMessageCodes;
}
@Override
public void onReceivedSslError(WebView view, @NonNull final SslErrorHandler handler, @NonNull SslError error) {
List<Integer> errorCodeMessageCodes = getAllSslErrorMessageCodes(error);
StringBuilder stringBuilder = new StringBuilder();
for (Integer messageCode : errorCodeMessageCodes) {
stringBuilder.append(" - ").append(mActivity.getString(messageCode)).append('\n');
}
String alertMessage =
mActivity.getString(R.string.message_insecure_connection, stringBuilder.toString());
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle(mActivity.getString(R.string.title_warning));
builder.setMessage(alertMessage)
.setCancelable(true)
.setPositiveButton(mActivity.getString(R.string.action_yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
handler.proceed();
}
})
.setNegativeButton(mActivity.getString(R.string.action_no),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
handler.cancel();
}
});
builder.create().show();
}
@Override
public void onFormResubmission(WebView view, @NonNull final Message dontResend, @NonNull final Message resend) {
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle(mActivity.getString(R.string.title_form_resubmission));
builder.setMessage(mActivity.getString(R.string.message_form_resubmission))
.setCancelable(true)
.setPositiveButton(mActivity.getString(R.string.action_yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
resend.sendToTarget();
}
})
.setNegativeButton(mActivity.getString(R.string.action_no),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dontResend.sendToTarget();
}
});
AlertDialog alert = builder.create();
alert.show();
}
@Override
public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull String url) {
// Check if configured proxy is available
if (!mProxyUtils.isProxyReady()) {
// User has been notified
return true;
}
Map<String, String> headers = mLightningView.getRequestHeaders();
if (mLightningView.isIncognito() && Utils.doesSupportHeaders()) {
view.loadUrl(url, headers);
return true;
}
if (url.startsWith("about:") && Utils.doesSupportHeaders()) {
view.loadUrl(url, headers);
return true;
}
if (url.startsWith("mailto:")) {
MailTo mailTo = MailTo.parse(url);
Intent i = Utils.newEmailIntent(mailTo.getTo(), mailTo.getSubject(),
mailTo.getBody(), mailTo.getCc());
mActivity.startActivity(i);
view.reload();
return true;
} else if (url.startsWith("intent://")) {
Intent intent;
try {
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
} catch (URISyntaxException ignored) {
intent = null;
}
if (intent != null) {
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
intent.setSelector(null);
}
try {
mActivity.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.e(Constants.TAG, "ActivityNotFoundException");
}
return true;
}
}
if (!mIntentUtils.startActivityForUrl(view, url) && Utils.doesSupportHeaders()) {
view.loadUrl(url, headers);
}
return Utils.doesSupportHeaders() || super.shouldOverrideUrlLoading(view, url);
}
}
@@ -0,0 +1,62 @@
package acr.browser.lightning.view;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.AutoCompleteTextView;
public class SearchView extends AutoCompleteTextView {
public interface PreFocusListener {
void onPreFocus();
}
@Nullable private PreFocusListener mListener;
private boolean mIsBeingClicked;
private long mTimePressed;
public SearchView(Context context) {
super(context);
}
public SearchView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SearchView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setOnPreFocusListener(@Nullable PreFocusListener listener) {
mListener = listener;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mTimePressed = System.currentTimeMillis();
mIsBeingClicked = true;
break;
case MotionEvent.ACTION_CANCEL:
mIsBeingClicked = false;
break;
case MotionEvent.ACTION_UP:
if (mIsBeingClicked && !isLongPress()) {
if (mListener != null) {
mListener.onPreFocus();
}
}
break;
}
return super.onTouchEvent(event);
}
private boolean isLongPress() {
return (System.currentTimeMillis() - mTimePressed) >= ViewConfiguration.getLongPressTimeout();
}
}

Alguns arquivos não foram exibidos porque demasiados arquivos foram alterados neste diff Mostrar Mais