624 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
Anthony Restaino 8061d8726a Add clear button to the search bar instead of go button 2015-08-30 15:23:59 -04:00
Anthony Restaino 1896fa6151 Animations for activity transitions 2015-08-27 22:44:22 -04: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
Anthony Restaino 98f0daceaa method could be static 2015-08-25 21:02:37 -04:00
Anthony Restaino 367c62bd39 Improved reading mode thanks to changes from snacktory fork by skyshard 2015-08-25 20:59:23 -04:00
Anthony Restaino 04c9f75a90 Added option for empty user agent if the user sets an empty string to work around webview limitations 2015-08-25 20:19:38 -04:00
Anthony Restaino dd18526ddf Fixed some deprecation problems and code analysis warnings 2015-08-23 23:26:21 -04:00
Anthony Restaino 85d92db738 Switched to RecyclerView, cleaned up some HTML generator methods 2015-08-23 19:21:22 -04:00
Anthony Restaino b68ad65abc Added permission handling and support for API 23 2015-08-23 12:13:06 -04:00
Anthony Restaino a0ade8acc9 Changed padding on toolbar for consistency, updated build tools 2015-08-22 10:04:35 -04:00
Anthony Restaino 676ba822af Try to fix problem with netcipher library 2015-08-22 09:24:58 -04:00
Anthony Restaino 1e385ceb9a Added back sdk 22 necessary for orbot library 2015-08-22 09:11:18 -04:00
Anthony Restaino 9f2f9d74eb First step toward Android M support, compile with sdk 23, fix errors caused by upgrade
removed copy button from search bar and replaced with go action. Had to
remove browser content provider usage as it is not longer included in
the sdk and has been completely removed.
2015-08-22 09:08:39 -04:00
Anthony Restaino 68f5c4fb45 Better URL validation, thanks AOSP 2015-08-21 21:55:55 -04:00
Anthony Restaino a08d793320 Added homepage button, altered tab UI slightly, fixed URL validation 2015-08-21 21:33:45 -04:00
Anthony Restaino d8fc799586 Merge branch 'master' into dev 2015-08-21 17:59:28 -04:00
Anthony Restaino f3b0e46801 Fixed many code analysis warnings 2015-08-21 17:55:58 -04:00
Anthony Restaino d5e1e06d84 Fixed bug where history wasn't being deleted until the app was restarted 2015-08-21 17:15:08 -04:00
Anthony Restaino 119245a5fa removed unused strings 2015-08-21 17:07:03 -04:00
Anthony Restaino ff5810c89a another attempt to fix travis 2015-08-21 16:57:19 -04:00
Anthony Restaino 2aa03a87a6 added info about contributing 2015-08-21 16:47:08 -04:00
Anthony Restaino d6fbfeaf29 Attempt to fix continuous integration build error 2015-08-21 16:43:20 -04:00
Anthony Restaino 88f07e3ced Update travis CI settings to support containers for faster building 2015-08-21 16:14:14 -04:00
Anthony Restaino c301f3963a Fixed a couple code warnings 2015-08-21 16:05:10 -04:00
Anthony Restaino 0a67f9e92a Renamed folders to fix build error 2015-08-21 16:04:23 -04:00
Anthony Restaino c9579b9d82 Merge branch 'master' into dev
Conflicts:
	app/src/main/res/values-it/strings.xml
2015-08-21 15:54:13 -04:00
Anthony Restaino f39631bd23 Merge pull request #283 from yuki2006/dev
fix: about scheme
2015-08-20 21:00:53 -04:00
Anthony Restaino f963b4de7f Merge pull request #282 from MarkThat/patch-4
Same as branch master, every string is updated.
2015-08-20 21:00:22 -04:00
Anthony Restaino 6be3cab470 Merge pull request #281 from MarkThat/patch-1
Changed a few old strings and added new.
2015-08-20 21:00:05 -04:00
Anthony Restaino b619a12ae3 Miscellaneous code analysis warning fixes 2015-08-20 20:59:24 -04:00
Anthony Restaino 58c9e820ed Initial support for tabs on the top instead of in the navigation drawer
added a setting to switch between modes. Still needs work to be less
buggy
2015-08-20 20:58:33 -04:00
ono 33eb739824 fix: about scheme 2015-08-20 14:23:20 +09:00
Mark. 81cef51479 Same as branch master, every string is updated. 2015-08-19 12:35:09 +02:00
Mark. 839616a4e4 Changed a few old strings and added new. 2015-08-18 19:05:40 +02:00
Anthony Restaino e71e09c2e8 Further generify Adblock host loading 2015-08-17 19:23:09 -04:00
Anthony Restaino 25a80a86a5 Update hosts file, create versatile hosts loading method to make way for users to load from any hosts file 2015-08-16 17:14:48 -04:00
Anthony Restaino 59c720d7d8 Fixed a setting, lowered priority on a thread 2015-08-12 21:01:52 -04:00
Anthony Restaino 7e67770617 Implement javascript close window method in the browser 2015-08-11 19:59:01 -04:00
Anthony Restaino c4e244a82b Make incognito mode safer, fix crash in search adapter, 2015-08-10 20:57:01 -04:00
Anthony Restaino 29a20a7e58 Updated with latest changes from NetCipher library 2015-08-06 19:21:00 -04:00
Anthony Restaino a738308a50 Added korean translation 2015-08-05 22:49:42 -04:00
Anthony Restaino 5081ee2ea6 Fix RuntimeException on Android M Preview 2015-08-05 22:10:51 -04:00
Anthony Restaino 29d2a5f3e5 Use single WebkitProxy reset method 2015-08-05 21:34:02 -04:00
Anthony Restaino c39a835dc2 Update netcipher library to latest version 2015-08-05 20:06:19 -04:00
Anthony Restaino 4ba7c7c5a3 Fixed some bugs 2015-08-05 20:04:28 -04:00
Anthony Restaino 08eedbe121 Add option to clear Web Storage 2015-08-04 20:08:55 -04:00
Anthony Restaino 4a32f0dfba Merge pull request #271 from anthologist/master
Updated italian translation
2015-08-04 20:08:45 -04:00
anthologist f101ea34ce Update strings.xml 2015-08-04 16:35:23 +02:00
anthologist 9f036410d2 Update strings.xml 2015-08-04 12:14:21 +02:00
anthologist 86834fca60 Update strings.xml 2015-08-04 12:11:25 +02:00
Anthony Restaino 3b13999b03 Added text encoding setting, updated support libraries 2015-08-03 22:33:11 -04:00
anthologist cc78b4196f Update strings.xml 2015-08-03 19:23:17 +02:00
Anthony Restaino b8b610347f fixed full-screen mode when watching a video in full-screen 2015-08-02 11:42:18 -04:00
Anthony Restaino 24a99deb52 Add suggestions to naming a folder in edit bookmark dialog 2015-07-31 21:37:26 -04:00
Anthony Restaino 0f9a69ba17 Updated hosts file 2015-07-30 20:26:43 -04:00
Anthony Restaino 71fcd174d7 fixed bug in search adapter 2015-07-29 22:46:59 -04:00
Anthony Restaino 399037d49b Fixed bug when long pressing bookmarks, removed unused strings 2015-07-29 22:01:01 -04:00
Anthony Restaino 240c9a5a37 Merge remote-tracking branch 'origin/master' into dev
Conflicts:
	app/src/main/res/values-fr/strings.xml
2015-07-29 21:44:33 -04:00
Anthony Restaino 4a3362e8f1 Merge pull request #268 from David-Guillot/patch-1
French translations, first batch
2015-07-29 21:38:38 -04:00
Anthony Restaino 7331345348 More clear and understandable suggestions filtering algorithm 2015-07-29 21:38:23 -04:00
Anthony Restaino 73e8f7c314 long-press on a folder on the bookmarks page works correctly now + other cleanup 2015-07-27 22:50:14 -04:00
Anthony Restaino aced4a3cc7 Sort bookmarks so folders are at the end of the list, updated bookmarks page to better utilize space 2015-07-26 20:19:47 -04:00
Anthony Restaino 69deb5b5a2 Renaming and Deleting bookmark folders is now available yay 2015-07-26 13:49:45 -04:00
David Guillot 3a76439a65 Fixed forgotten escaping 2015-07-26 14:41:47 +02:00
David Guillot fa7b04adee Fixed typo
Thanks @kuc
2015-07-26 14:36:32 +02:00
David Guillot a708050c5b French translations, first batch 2015-07-26 13:11:40 +02:00
Anthony Restaino 645b98cd50 Simplify and improve long press handling for links on the bookmark page and history page 2015-07-25 22:05:09 -04:00
Anthony Restaino 19103e9b2c Added Bookmark folders, Added actions to the bookmark drawer, + other
Updated icons, removed light/dark versions only have one version now
that uses a color filter to be themed to save space, optimized view
layouts
2015-07-25 10:19:14 -04:00
Anthony Restaino dce29954e1 Down with Toast, all hail Snackbar 2015-07-19 16:58:34 -04:00
Anthony Restaino f061a35472 Snackbar >>>>>>> Toast 2015-07-19 16:49:55 -04:00
Anthony Restaino 800d037035 Removed use of tabs and replaced with 4 spaces 2015-07-19 15:42:14 -04:00
Anthony Restaino e35b368d50 Updated ProxyUtils to automatically start TOR when needed, more abstraction of BrowserActivity, other cleanup 2015-07-19 15:36:41 -04:00
Anthony Restaino aa21657875 Fixed errors with ProxyUtils 2015-07-18 17:59:43 -04:00
Anthony Restaino f314b64e40 updated to latest netcipher submodule 2015-07-18 16:59:57 -04:00
Anthony Restaino 6c9d23488b Merge remote-tracking branch 'origin/master' into dev
Conflicts:
	app/src/main/res/values-pt/strings.xml
2015-07-18 16:43:42 -04:00
Anthony Restaino 41cb2c4d27 Convert BrowserActivity to an abstract class, remove unused resources 2015-07-18 16:38:57 -04:00
Anthony Restaino 969cab81e7 New Full-screen mode works better and doesn't hide the top of the WebView, +other
* Now using material alertdialog on all versions
* cleaned up some code
* fixed lint issues and other inspection related problems
* Attempted to fix bugs found
2015-07-18 14:30:41 -04:00
Anthony Restaino b98dd272c0 Merge pull request #255 from DF1E/settings
Material Settings
2015-07-18 10:05:35 -04:00
Anthony Restaino 67b506c5f8 Merge pull request #254 from smarquespt/master
Portuguese update
2015-07-17 22:18:09 -04:00
Anthony Restaino e9af39a20b Merge pull request #260 from chirs1985/master
1. Use the " \ " character to escape the " ' " character
2015-07-17 22:16:04 -04:00
chirs1985 5950c66567 1. Use the " \ " character to escape the " ' " character
2. lose the </string-arrary>
2015-07-06 11:06:39 +08:00
DF1E 56c9934145 remove LicenseActivity intent 2015-06-16 15:13:27 +02:00
DF1E 1d5b904c64 create PreferenceCategory for licenses 2015-06-15 18:39:24 +02:00
DF1E 7d9f382333 Toolbar fix 2015-06-14 12:13:35 +02:00
DF1E c60b4389a0 small fix
this info dialog is not necessary because the flash CheckBox isn't
enabled if API>19
2015-06-13 19:37:07 +02:00
DF1E 6b80df1d27 clean manifest 2015-06-12 17:10:46 +02:00
DF1E b60f555553 new Settings 3/3
now we have to bring the ToolBar back and fix some bugs...
And testing!
2015-06-12 17:10:23 +02:00
DF1E 2127863465 new Settings 2/3 2015-06-12 14:00:36 +02:00
DF1E d47a86d9b5 new Settings 1/3 2015-06-12 13:14:29 +02:00
DF1E a03444f4d0 rebase test for main settings screen 2015-06-11 18:48:24 +02:00
Sérgio Marques 86b8eaee12 Portuguese update 2015-06-09 10:21:35 +01:00
Anthony Restaino d8b8d2c047 Moved proxy code to utility class, remove proxy from lite version to reduce apk size 2015-06-05 00:03:02 -04:00
Anthony Restaino 9dc9634299 Fixed bug 2015-06-03 11:00:54 -04:00
Anthony Restaino e13e2dd006 Fix error in strings 2015-06-03 10:58:54 -04:00
Anthony Restaino 4c7bb196dd Merge remote-tracking branch 'origin/master' into dev 2015-06-03 10:55:53 -04:00
Anthony Restaino a2f2fbc82b Merge pull request #249 from str4d/i2p-android
Support I2P (and other proxies in future)
2015-06-03 10:54:11 -04:00
Anthony Restaino 80d765b61c Merge remote-tracking branch 'origin/master' into dev 2015-06-03 10:52:34 -04:00
Anthony Restaino b478c1ea98 Code cleanup 2015-06-03 10:50:51 -04:00
Anthony Restaino 7e5dbbc811 Merge pull request #251 from MarkThat/patch-3
Patch 3
2015-06-03 10:49:00 -04:00
str4d 2eec8be4ce Add manual proxy picker 2015-06-03 06:50:32 +00:00
Mark. 0c10efec00 Corrections.
Removed few strings which i added for error. Can these changes be merged without gradle?  I don't understand how to use it hehe.
Cheers!
2015-05-30 11:39:41 +02:00
Mark. 2cbe2e80e7 Update strings.xml 2015-05-29 15:36:05 +02:00
Mark. e7029a7b64 Update.
I found some errors and fixed them and added the new translations.
2015-05-29 15:34:19 +02:00
Anthony Restaino 1e0770057e Merge pull request #241 from AltNico/cleanup
Remove unnecessary files
2015-05-27 07:21:13 -04:00
str4d 810483ec74 Use constants for proxy choices, part 2 2015-05-26 12:52:11 +00:00
str4d a0b2197d8f Use constants for proxy choices 2015-05-26 10:52:58 +00:00
str4d a5a20eebbd Notify user if proxy is not ready when they try to load a URL 2015-05-26 10:36:02 +00:00
str4d 6e8da9f6d3 Fix for I2PAndroidHelper.isI2PAndroidRunning() always returning false
Requires v0.7 of the I2P client library, which will be released once I2P 0.9.20
is released.
2015-05-26 03:25:47 +00:00
Nico Alt c74b84d070 remove unnecessary files
Basically, all I have done was adding some missing files/directories to
.gitignore and the executing the following commands:
git rm --cached -r ./
git add --all
2015-05-25 22:43:24 +02:00
Anthony Restaino e9203f20b3 Utilize gradle product flavors to produce free and plus versions 2015-05-25 12:55:35 -04:00
Anthony Restaino 4a38511218 Merge remote-tracking branch 'origin/master' into dev 2015-05-25 11:53:21 -04:00
Anthony Restaino 3517435956 Merge pull request #247 from Roboe/l10n-es
Updated Spanish (es) localization
2015-05-25 11:51:08 -04:00
Anthony Restaino 8bb38d1a90 Merge pull request #238 from ageback/master
Complete S.Chinese.
2015-05-25 11:45:18 -04:00
str4d 111d594c6b Use I2P if configured 2015-05-25 12:09:36 +00:00
str4d 6c2a557135 Change Orbot checkbox to an HTTP proxy choice list (None, Orbot, I2P) 2015-05-24 13:20:08 +00:00
str4d 46fbc56604 Add I2P client library to dependencies 2015-05-24 13:19:02 +00:00
Roboe 0beec60b7f [Spanish l10n] Fixed duplicated string 2015-05-24 00:19:40 +02:00
Roboe 1de67105d9 [Spanish l10n] Added new strings 2015-05-23 17:37:45 +02:00
Roberto M.F. eacadcebba [Spanish l10n] Fixed some strings and updated untranslated ones 2015-05-22 18:09:49 +02:00
Anthony Restaino 9f8dff8c5d Added an AMOLED Black theme, changed from tabs to spaces for some files 2015-05-13 10:35:32 -04:00
Anthony Restaino b400ef0647 Merge pull request #237 from smarquespt/master
Portuguese update
2015-05-13 09:09:55 -04:00
Michael Lu 025714bd97 Complete S.Chinese. 2015-05-13 15:26:03 +08:00
Sérgio Marques 5e131a0a6c Portuguese update 2015-05-12 16:34:22 +01:00
Anthony Restaino 9677135c28 Merge pull request #234 from anthonycr/dev
compile jsoup from jcenter instead of embedding jar in code
2015-05-11 11:08:38 -04:00
Anthony Restaino a0268a9dfa compile jsoup from jcenter instead of embedding jar in code 2015-05-11 10:06:00 -04:00
Anthony Restaino 28498d7d92 Merge pull request #232 from anthonycr/dev
Dev
2015-05-10 17:03:57 -04:00
Anthony Restaino 2f3655045c Updated to latest gradle plugin 2015-05-10 16:35:21 -04:00
Anthony Restaino 459c5f8ff0 updated to latest netcipher library 2015-05-10 16:27:19 -04:00
Anthony Restaino e335a2b936 Remove unused resources, code cleanup 2015-05-05 20:57:42 -04:00
Anthony Restaino 051a453e7b Merge remote-tracking branch 'origin/master' into dev
Conflicts:
	app/src/main/res/values-sr/strings.xml
2015-05-04 12:30:17 -04:00
Anthony Restaino df903551c0 Merge branch 'pr/229' into dev
Conflicts:
	app/build.gradle
2015-05-04 12:23:51 -04:00
Anthony Restaino 99a23c2eef Remove unused resources on build 2015-05-04 12:07:15 -04:00
Anthony Restaino fa1994c8b2 Lint fixes and code cleanup 2015-05-04 12:06:51 -04:00
DF1E f5fcc2e62b improvements for android studio 2015-05-04 18:00:39 +02:00
Anthony Restaino cb27bf8afa Fix BrowserApp class not being found by webkitproxy 2015-05-03 00:11:46 -04:00
Anthony Restaino 51fc8d7f85 Fix missing strings error, remove unused icon 2015-05-02 23:40:47 -04:00
Anthony Restaino 13d85c0f90 Serbian translation thanks to @pekjam 2015-05-02 23:11:46 -04:00
Anthony Restaino 15fcb2ed62 Merge pull request #227 from pejakm/srupd
Update Serbian translation
2015-05-02 23:09:53 -04:00
Mladen Pejaković e6174e82d2 Update Serbian translation 2015-05-03 01:16:09 +02:00
Anthony Restaino f8c2d0096d Move java files to sub-packages for better organization 2015-05-02 16:37:22 -04:00
Anthony Restaino db734dfa7d Exclude support jars from netcipher build 2015-05-02 14:57:12 -04:00
Anthony Restaino e40f1d44a5 add m2repository 2015-05-02 13:25:27 -04:00
Anthony Restaino e7188c5985 Update build to search for latest support repos 2015-05-02 13:12:16 -04:00
Anthony Restaino 8738af72f0 Update to latest sdk 2015-05-02 13:05:32 -04:00
Anthony Restaino b02f726a32 Fix travis yml 2015-05-02 13:00:17 -04:00
Anthony Restaino 4868cf0cc6 Try to fix line end bug 2015-05-02 12:56:53 -04:00
Anthony Restaino fa77fe228a Dummy change to gradleq 2015-05-02 12:56:23 -04:00
Anthony Restaino e65e062652 Fixing travis 2015-05-02 12:51:14 -04:00
Anthony Restaino 27513bd94e Add in appropriate .idea files 2015-05-02 12:47:06 -04:00
Anthony Restaino d39bf65880 Remove unnecessary iml file 2015-05-02 12:44:09 -04:00
Anthony Restaino 8e32668305 Remove old iml file 2015-05-02 12:42:27 -04:00
Anthony Restaino ce3923d336 Switch to gradle!!! 2015-05-02 12:40:40 -04:00
Anthony Restaino 38bce6a9a0 Merge pull request #222 from anthonycr/dev
Dev -> master
2015-05-01 09:39:31 -04:00
Anthony Restaino 0c88fbaec8 Merge branch 'dev' of https://github.com/anthonycr/Lightning-Browser into dev 2015-05-01 09:31:46 -04:00
Anthony Restaino 5c1b765616 Clear webdata when the cookies are cleared 2015-05-01 08:58:11 -04:00
Anthony Restaino 79d32127f4 Merge pull request #215 from bebolint98/patch-1
Update strings.xml
2015-05-01 08:54:57 -04:00
Anthony Restaino e31f533bbb Merge pull request #214 from bebolint98/patch-2
Update strings.xml
2015-05-01 08:54:52 -04:00
Anthony Restaino 89515e4420 Merge pull request #217 from smarquespt/master
Add Portuguese language
2015-05-01 08:52:58 -04:00
bebolint98 35e57fa1a6 Update strings.xml 2015-04-29 19:56:49 +02:00
bebolint98 7789fc7480 Update strings.xml 2015-04-29 19:54:39 +02:00
bebolint98 0d12624cbb Update strings.xml 2015-04-29 15:38:47 +02:00
bebolint98 837dd64c2c Update strings.xml 2015-04-29 15:33:51 +02:00
Sérgio Marques d3ab62dce6 Add Portuguese language 2015-04-29 12:35:45 +01:00
Anthony Restaino 4fb1a50f03 Use boolean constant to make free/plus builds easier to generate 2015-04-28 19:17:48 -04:00
Anthony Restaino 2b59ea1906 Fixed bug that crashed dialogs 2015-04-28 18:57:10 -04:00
bebolint98 419e36ffd1 Update strings.xml 2015-04-28 17:55:17 +02:00
bebolint98 37afaf7035 Update strings.xml 2015-04-28 17:39:09 +02:00
bebolint98 b552dc116f Update strings.xml 2015-04-28 17:37:18 +02:00
bebolint98 128ebfab14 Update strings.xml 2015-04-28 17:35:09 +02:00
bebolint98 9ebd876f27 Update strings.xml 2015-04-28 17:19:15 +02:00
bebolint98 03fe3cdee5 Update strings.xml 2015-04-28 17:14:19 +02:00
Anthony Restaino e042830e17 Fixed bug where incognito tabs are rememebered 2015-04-27 21:01:37 -04:00
Anthony Restaino 4711fa696a Workaround for a bug in LG devices 2015-04-25 22:22:24 -04:00
Anthony Restaino f4078cce33 Merge branch 'master' of https://github.com/anthonycr/Lightning-Browser
Conflicts:
	res/values-hu/strings.xml
	res/values-it/strings.xml
	res/values-ja/strings.xml
2015-04-25 22:12:11 -04:00
Anthony Restaino 9d92a97fe2 Merge pull request #207 from astrone/patch-2
Made some extra corrections and translated one more string
2015-04-25 21:56:24 -04:00
Anthony Restaino c4c50467dd Merge pull request #208 from ys0115/patch-1
Update strings.xml
2015-04-25 21:56:11 -04:00
Anthony Restaino 792cb0d4d6 Merge pull request #210 from bebolint98/dev
Add hungarian localization to dev branch
2015-04-25 21:55:43 -04:00
bebolint98 981d263331 Update strings.xml 2015-04-25 22:58:10 +02:00
bebolint98 15c5703a3b Create strings.xml 2015-04-25 22:39:01 +02:00
Anthony Restaino a6a1baf41b Last updates for public release 4.0.8a. Updated adblocking hosts and added some default bookmarks for better UX 2015-04-25 13:53:25 -04:00
Anthony Restaino 38d44b8c2f Refactored/Cleaned up some settings activities 2015-04-25 10:45:43 -04:00
ys0115 56bd6e07fc Update strings.xml 2015-04-25 20:07:38 +09:00
astrone 6125e385df Made some corrections and translated one more string 2015-04-24 20:56:27 +02:00
Anthony Restaino 532860245d Fixed scrolling sensitivity in full-screen, fixed http auth dialog issue 2015-04-22 08:49:05 -04:00
Anthony Restaino 68a9b1de7e update submodules from netcipher 2015-04-21 14:51:06 -04:00
Anthony Restaino 97e2e8d79a Fix file uploading on Lollipop, clean up the code 2015-04-21 14:46:30 -04:00
Anthony Restaino be3a59c74c Optimize webpage builders, fixed bug in bookmark activity 2015-04-18 15:34:40 -04:00
Anthony Restaino 17a63733fe Merge pull request #203 from bebolint98/master
Add Hungarian localization
2015-04-18 13:27:08 -04:00
Anthony Restaino 3f5c08bb5a Merge pull request #204 from astrone/patch-2
Italian translation (Update)
2015-04-18 13:26:49 -04:00
Anthony Restaino 6f30103cd9 Organize PreferenceManager 2015-04-18 13:25:42 -04:00
Anthony Restaino 2d347074a6 Use a PreferenceManager to handle SharedPreferences among classes 2015-04-18 13:21:33 -04:00
Anthony Restaino bd8d2ee0b8 Spring Cleaning 2015-04-17 14:54:20 -04:00
Anthony Restaino c1baab8c9c Remove unused class 2015-04-17 08:49:55 -04:00
Anthony Restaino 4e5eac4d5b Revert "Revert "Fixed bug in dark mode where search suggestions wouldn't show up. Sped up app startup by using singleton in BookmarkManager.""
This reverts commit cc6d7c7aa9.
2015-04-17 08:47:08 -04:00
Anthony Restaino cc6d7c7aa9 Revert "Fixed bug in dark mode where search suggestions wouldn't show up. Sped up app startup by using singleton in BookmarkManager."
This reverts commit 4822996da1.
2015-04-17 08:46:37 -04:00
Anthony Restaino 4822996da1 Fixed bug in dark mode where search suggestions wouldn't show up. Sped up app startup by using singleton in BookmarkManager. 2015-04-17 08:46:16 -04:00
bebolint98 546dbe4f8c Update strings.xml 2015-04-10 18:43:57 +02:00
astrone 013011ef09 Update strings.xml 2015-04-07 12:51:35 +02:00
bebolint98 344b662619 Update strings.xml 2015-04-05 18:16:03 +02:00
bebolint98 93f8b971e7 Update strings.xml 2015-04-05 17:31:45 +02:00
bebolint98 b135e27fd9 Update strings.xml 2015-04-04 22:15:07 +02:00
bebolint98 38f3a9c9df Update strings.xml 2015-04-04 22:11:45 +02:00
bebolint98 80d019798d strings.xml 2015-04-04 20:55:04 +02:00
bebolint98 1727d0739e Delete values-hu 2015-04-04 19:18:32 +02:00
bebolint98 7ab948ce3f Create values-hu 2015-04-04 19:18:00 +02:00
Anthony Restaino 8c29cb4450 Bugfixes, code clean up 2015-04-04 11:27:34 -04:00
astrone af4fa8ed2a Few new strings added and updated a pair.
There is an error in Privacy Settings with italian translation. Here a screenshot: https://db.tt/IPw7eaf5 
I have fixed it however i could not find the new strings "Tabs" , "Reader mode" etc.. hope to see them soon!
2015-04-04 11:36:15 +02:00
ys0115 ab5a118947 Update strings.xml 2015-04-03 13:49:53 -04:00
Ivan Markin c8dec7b305 added new strings 2015-04-03 13:49:53 -04:00
Ivan Markin c6d0a1a788 Fixed Russian translation 2015-04-03 13:49:52 -04:00
Luigigimmi 3239bcefe3 Update strings.xml 2015-04-03 13:49:52 -04:00
ys0115 2790664866 Create strings.xml
Japanese
2015-04-03 13:49:51 -04:00
Anthony Restaino bad6872f69 Improved TextReflow on Kitkat and up 2015-04-03 09:14:29 -04:00
Anthony Restaino d9e888e8a9 Change Navigation Drawer layout slightly, fix dark theme for drawer 2015-04-02 21:50:59 -04:00
Anthony Restaino 5dafd6f815 Merge pull request #202 from DF1E/theme
simplify theming
2015-04-02 20:40:24 -04:00
DF1E addaa3b2b3 fix theme on BrowserActivity
there should be only one ThemableActivity but there are two dark themes
- one for settings and on for BrowserActivity
2015-04-01 22:13:46 +02:00
DF1E 2551b3dc27 improve restart 2015-04-01 21:35:16 +02:00
DF1E aedf76e3ae improve theming 2 2015-04-01 21:31:28 +02:00
DF1E 625fbb1aa9 improve theming 1 2015-04-01 21:22:42 +02:00
DF1E c75ca89775 clean BrowserActivity 2015-04-01 20:36:10 +02:00
Anthony Restaino 763524555b Added preference changes due to theme changes. 2015-03-31 11:21:07 -04:00
Anthony Restaino a4f0c010d1 Added Dark Theme to browser. Added options to Reading Mode. 2015-03-31 11:20:41 -04:00
Anthony Restaino 1d6a445d33 Fix deprecation issues, fix a couple rendering issues 2015-03-29 18:03:33 -04:00
Anthony Restaino 7defcff9b1 Update icon 2015-03-29 00:48:39 -04:00
Anthony Restaino 5944cdc5df Lint fixes, new icon, fixes for SearchAdapter showing weird on ICS 2015-03-29 00:46:24 -04:00
Anthony Restaino 5e6a654170 Add back/forward arrows for large devices, change arrow colors to light for dark theme 2015-03-29 00:26:32 -04:00
Anthony Restaino 450ba6b0fd Update project for new API 22, remove another layer of overdraw on the WebView 2015-03-28 15:39:18 -04:00
Anthony Restaino c87c57661f Remove overdraw and stop blocking DOM storage (breaks sites) in incognito mode 2015-03-27 20:55:48 -04:00
Anthony Restaino 4699b583f0 Display shadow behind progress bar for non Lollipop devices 2015-03-26 18:46:38 -04:00
Anthony Restaino 9ff1614a0f Cache favicons when they are downloaded by the WebView for use by bookmarks 2015-03-26 18:46:14 -04:00
Anthony Restaino 58ca7fa303 Display back and forward buttons on tablets 2015-03-26 18:45:38 -04:00
Anthony Restaino 1f1ed20a7e Cache search suggestions temporarily so that repeated searches to not waste data requests 2015-03-26 12:37:29 -04:00
Anthony Restaino 8b3da70d92 Rename HistoryDatabase and convert it to a singleton for easier usage. Improved database structure. 2015-03-26 11:09:09 -04:00
Anthony Restaino f2f6f2761c Reduce overdraw on dropdown view 2015-03-25 21:56:52 -04:00
Anthony Restaino ecdf533188 Color mode dynamically lightens colors that are too dark to see 2015-03-25 20:51:29 -04:00
Anthony Restaino 0a4f650869 Switch out "Android Search" for better known Ask.com search 2015-03-24 20:45:34 -04:00
Anthony Restaino 32f4a457bb Fixed bug where progress bar didn't fade out always 2015-03-24 20:43:25 -04:00
Anthony Restaino 0116481022 Using a smoother progress bar 2015-03-24 16:13:08 -04:00
Anthony Restaino dfb0febbe7 Merge pull request #197 from ys0115/master
Add Japanese localization
2015-03-23 18:08:15 -04:00
Anthony Restaino b87eb5e90e Merge pull request #199 from DF1E/dev
complete german translation
2015-03-23 18:08:07 -04:00
Anthony Restaino 666294834a Merge pull request #196 from mark-in/ru-correct
Corrections for Russian localization
2015-03-23 18:07:56 -04:00
DF1E 3870e8c156 complete german translation 2015-03-14 11:37:10 +01:00
ys0115 86d83b887a Update strings.xml 2015-03-07 10:08:00 +09:00
Ivan Markin 4cc65d0d77 added new strings 2015-02-27 03:29:42 +03:00
Ivan Markin 095810671d Fixed Russian translation 2015-02-27 03:15:46 +03:00
Anthony Restaino 5fb00c08c2 Added in option to change URL display (url, domain, title), and other minor changes
Additional changes include removing useless code and making some utility
methods not reliant on Context
2015-02-24 13:52:17 -05:00
Anthony Restaino 88e5a0eabb Merge pull request #195 from Luigigimmi/patch-1
Update strings.xml
2015-02-23 12:58:09 -05:00
Luigigimmi 698693586c Update strings.xml 2015-02-22 11:00:09 +01:00
Anthony Restaino f1cc80eb28 Merge pull request #191 from DF1E/fix-readingactivity
fix ReadingActivity
2015-02-18 13:54:56 -05:00
ys0115 11d94564de Create strings.xml
Japanese
2015-02-14 22:47:57 +09:00
DF1E a21e2f6a7c fix ReadingActivity
I had no problems on stock android but on cyanogenmod I got crashes
without this
2015-02-12 18:28:34 +01:00
Anthony Restaino 971b0cd022 Change Reading mode package name to lower case 2015-02-09 15:46:10 -05:00
Anthony Restaino d60fe82b4a Fix lint problems and other code style problems, also fixed sluggish navigation drawer issues 2015-02-09 15:45:40 -05:00
Anthony Restaino 0c57e14f05 Add in proguard protection for Reading Mode (should keep it from crashing) 2015-02-09 15:31:16 -05:00
Anthony Restaino fa3c784722 Add in attribution to jsoup library 2015-02-09 15:30:50 -05:00
Anthony Restaino a4878914e2 Remove mdpi assets, just rely on scaled down hdpi versions 2015-02-09 15:30:11 -05:00
Anthony Restaino db20a4eeac Fixed problem where progress bar didn't display on 4.0 and 4.1 2015-02-05 20:59:52 -05:00
Anthony Restaino 10668a019b Added a Reading Mode that can be accessed from the menu
Reading Mode utilizes the Snacktory library created by karussel which is
licensed under the Apache 2.0 license.
https://github.com/karussell/snacktory
2015-02-05 15:33:23 -05:00
Anthony Restaino 313f9fb105 Fixed bug where navigation drawers sometimes overlapped 2015-02-05 12:21:16 -05:00
Anthony Restaino e7dacc9c10 Attempt to fix bug where DrawerArrowDrawable animation gets stuck half way.
Also, add a background to indicate that the exit button on a tab is
pressed.
2015-02-05 12:09:39 -05:00
Anthony Restaino 9173e8270a Merge branch 'dev' of https://github.com/anthonycr/Lightning-Browser into dev 2015-02-04 21:30:29 -05:00
Anthony Restaino ab134a8927 Fixed bug with Palette API 2015-02-04 21:30:25 -05:00
Anthony Restaino 71471f0718 Merge pull request #182 from karolba/patch-1
Update Polish translation in dev branch
2015-02-04 13:54:48 -05:00
karolba 87ee80fc8b Fix spelling 2015-02-04 17:07:45 +01:00
karolba 29d55ec890 Update Polish translation in dev branch 2015-02-04 09:34:28 +01:00
Anthony Restaino 9eedc19b11 Merge pull request #176 from kuc/fix-travis-build
Fix Travis build in dev branch
2015-02-03 08:22:50 -05:00
Miłosz Sieradzki 675df18a7d Port changes from setup-ant.sh to setup-ant.bat 2015-02-01 18:55:32 +01:00
Miłosz Sieradzki 382cfdbc65 Change libraries to library projects 2015-02-01 18:04:27 +01:00
Miłosz Sieradzki ddfc5d9334 Finish updating target to android-21 2015-02-01 13:05:40 +01:00
Anthony Restaino 5edbff4f39 Update .travis.yml 2015-01-31 23:36:10 -05:00
Anthony Restaino 86aefa5e54 Update setup-ant.sh 2015-01-31 23:36:05 -05:00
Anthony Restaino 1e647c8e78 Merge pull request #175 from anthonycr/master
Merge in travis build script updates
2015-01-31 23:23:03 -05:00
Anthony Restaino 4f1a1f3aa9 Merge pull request #171 from bidu-dw/dev
Change several search engine URLs to https
2015-01-31 23:11:22 -05:00
Anthony Restaino cbfacffff7 Merge pull request #173 from DF1E/dev
update german string
2015-01-31 23:09:23 -05:00
Anthony Restaino ac48ddfbce Merge pull request #174 from kuc/update-travis-build-script
Update Travis build script
2015-01-31 23:05:11 -05:00
Anthony Restaino 489a814f54 Changes to make Incognito mode more secure and less likely to leak data to websites.
Changes for Incognito Settings
* Always disable location (even if explicitly set in settings)
* Never save passwords or form data
* Always set mixed content mode to NEVER ALLOW
* Disable DOM storage
2015-01-31 22:36:19 -05:00
Anthony Restaino 43950d4f71 Bug Fixes for rouge Android versions/OEMs that don't behave correctly 2015-01-31 11:01:24 -05:00
Miłosz Sieradzki 76178eddc6 Update Travis build script 2015-01-31 13:47:19 +01:00
DF1E cec457fc37 update german string 2015-01-31 10:20:08 +01:00
bidu-dw 76ab43743d Change several search engine URLs to https
Yahoo, Bing, Baidu and Yandex support https. Change for more safety.
2015-01-31 12:59:34 +08:00
Anthony Restaino 9ffeecc584 Improve rendering 2015-01-30 22:50:31 -05:00
Anthony Restaino 0e212539e9 Fixed occasional IllegalStateException 2015-01-30 22:50:15 -05:00
Anthony Restaino 6407f1101a Cache objects to use less memory 2015-01-30 22:49:55 -05:00
Anthony Restaino 03ac2f8b42 Final updates for second Lollipop beta 2015-01-30 21:25:09 -05:00
Anthony Restaino 35c585b3f4 Search adapter shouldnt spawn new worker threads if current ones havent finished 2015-01-29 21:57:39 -05:00
Anthony Restaino 04c4d202b2 Add utility method to help finding favicons 2015-01-29 21:57:19 -05:00
Anthony Restaino 6269325a44 Revert "Revert "Fix miscellaneous lint errors""
This reverts commit bd308dead7.
2015-01-29 21:56:48 -05:00
Anthony Restaino bd308dead7 Revert "Fix miscellaneous lint errors"
This reverts commit 920113b49a.
2015-01-29 21:56:35 -05:00
Anthony Restaino 920113b49a Fix miscellaneous lint errors 2015-01-29 21:56:10 -05:00
Anthony Restaino 7f8253b470 Settings should utilize .apply() instead of .commit() 2015-01-29 21:55:46 -05:00
Anthony Restaino 17afd700d8 Update with new hosts file 2015-01-29 21:54:04 -05:00
Anthony Restaino 7e23135824 Simplified and improved filter algorithm 2015-01-29 20:31:33 -05:00
Anthony Restaino 8314676918 Added option to enable/disable Color Mode 2015-01-29 15:39:53 -05:00
Anthony Restaino c2b436ecfe Allow Sharing in Incognito Mode 2015-01-29 14:30:02 -05:00
Anthony Restaino a897ae4d3e Added option to block third party cookies 2015-01-29 13:52:56 -05:00
Anthony Restaino 9853804fd8 Complete Material Design for Settings 2015-01-29 13:25:19 -05:00
Anthony Restaino 42de0b3ae7 Fixed potential NullPointerExceptions
Rather than try to correct the issue of the Comparator crashing in
BookmarkManager because the Strings/HistoryItems were null, I modified
the HistoryItem object so that the title, url, and folder strings can no
longer be null but will instead be empty if set to null, this then
prevents the BookmarkManager from throwing an NPE when sorting the items
by title.
2015-01-29 09:26:46 -05:00
Anthony Restaino 1eed3ca948 Fixed potential NullPointerExeptions 2015-01-28 21:17:44 -05:00
Anthony Restaino 8be2b62601 Fixed styling issues with the toolbar 2015-01-28 21:17:25 -05:00
Anthony Restaino a201f88906 Merge pull request #168 from DF1E/dev
material design for erveryone..
2015-01-28 19:02:16 -05:00
DF1E a292c6c776 material design for erveryone..
use appcompat on all API's because of its backward compatibility
2015-01-28 19:16:00 +01:00
Anthony Restaino b4714a17c8 Update manifest to latest update 2015-01-27 20:28:03 -05:00
Anthony Restaino eff7e3800d Retrieve correct package name rather than hardcoding it 2015-01-27 20:27:54 -05:00
Anthony Restaino bdb36be4e4 Modified icon 2015-01-27 20:26:24 -05:00
Anthony Restaino 3c9f63b0a6 Simplify main activity layout file 2015-01-27 19:18:09 -05:00
Anthony Restaino 61d569bb7d Merge branch 'dev' of https://github.com/anthonycr/Lightning-Browser into dev 2015-01-27 11:38:59 -05:00
Anthony Restaino 376ac564b8 Animate toolbar hide/show 2015-01-27 11:38:13 -05:00
Anthony Restaino 785449fad6 Update README.md 2015-01-26 13:35:29 -05:00
Anthony Restaino c9e2026526 Merge pull request #167 from anthonycr/master
Add specific build information for each branch
2015-01-26 13:28:02 -05:00
Anthony Restaino ced119f311 Initial changes for Material Design 2015-01-26 13:09:27 -05:00
Anthony Restaino 7b8088f3d4 Activities now return correct menus that can be utilized by the toolbar 2015-01-26 13:07:57 -05:00
Anthony Restaino 762fe5b55b Add text reflow for Android versions >= Kitkat 2015-01-26 13:06:39 -05:00
Anthony Restaino fd781f4a63 JavaScript for inverted rendering 2015-01-26 13:06:02 -05:00
Anthony Restaino dd919513c1 Added material design drop shadow to html pages 2015-01-26 12:58:10 -05:00
378 arquivos alterados com 71618 adições e 32883 exclusões
-22
Ver Arquivo
@@ -1,22 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto
# Custom for Visual Studio
*.cs diff=csharp
*.sln merge=union
*.csproj merge=union
*.vbproj merge=union
*.fsproj merge=union
*.dbproj merge=union
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
+36 -147
Ver Arquivo
@@ -1,165 +1,54 @@
#################
## Eclipse
#################
*.pydevproject
.project
.metadata
bin/
tmp/
gen/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.classpath
.settings/
.loadpath
proguard/
# Android Studio
*.jks
.DS_Store
/local.properties
/.idea/workspace.xml
/build
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# CDT-specific
.cproject
# PDT-specific
.buildpath
#################
## Visual Studio
#################
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Rr]elease/
*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.vspscc
.builds
*.dotCover
## TODO: If you have NuGet Package Restore enabled, uncomment this
#packages/
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
# Visual Studio profiler
*.psess
*.vsp
# ReSharper is a .NET coding add-in
_ReSharper*
# Installshield output folder
[Ee]xpress
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish
# Others
[Bb]in
[Oo]bj
sql
TestResults
*.Cache
ClientBin
stylecop.*
~$*
*.dbmdl
Generated_Code #added for RIA/Silverlight projects
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
############
## Windows
############
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Local configuration file (sdk path, etc)
local.properties
gradle.properties
.directory
# Intellij project files
*.ipr
*.iws
.idea/
#############
## Python
#############
*.py[co]
# Packages
*.egg
*.egg-info
dist
# Gradle
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
.gradle
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg
# Mac crap
# https://gist.github.com/AltNico/c581f370b3f88715876b
*.apk
*.ap_
*.dex
*.class
build.xml
.DS_Store
gen/
.gradle/
proguard/
out
.settings/
*.swp
*~
# Source:
# https://raw.githubusercontent.com/github/gitignore/master/Android.gitignore
# https://gitlab.com/fdroid/fdroidclient/raw/master/.gitignore
*.iml
+10 -4
Ver Arquivo
@@ -1,15 +1,21 @@
language: android
sudo: false
android:
components:
- build-tools-19.1.0
- android-19
- tools
- build-tools-23.0.3
- build-tools-22.0.1
- android-23
- android-22
- extra-android-support
- extra-android-m2repository
licenses:
- 'android-sdk-license-.+'
- '.*intel.+'
before_install:
- chmod +x gradlew
- git submodule update --init --recursive
install:
- ./setup-ant.sh
- ./gradlew
script:
- ant debug && lint -w --exitcode --disable MissingTranslation .
- ./gradlew assembleDebug --stacktrace
-211
Ver Arquivo
@@ -1,211 +0,0 @@
<?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"
android:versionCode="66"
android:versionName="3.2.0.1a" >
<uses-permission android:name="android.permission.INTERNET" />
<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="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-feature
android:name="android.hardware.location.gps"
android:required="false" />
<uses-feature
android:name="android.hardware.location"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="20" />
<application
android:name="acr.browser.lightning.BrowserApp"
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:name="acr.browser.lightning.MainActivity"
android:alwaysRetainTaskState="true"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/LightTheme" >
<intent-filter>
<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" />
</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" />
<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" />
</intent-filter>
<!-- For viewing saved web archives. -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<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" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.WEB_SEARCH" />
<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" />
</intent-filter>
</activity>
<activity
android:name="acr.browser.lightning.SettingsActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/settings"
android:theme="@style/DefaultTheme" >
<intent-filter>
<action android:name="android.intent.action.SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="acr.browser.lightning.GeneralSettingsActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/settings_general"
android:theme="@style/DefaultTheme" >
<intent-filter>
<action android:name="android.intent.action.GENERAL_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="acr.browser.lightning.DisplaySettingsActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/settings_display"
android:theme="@style/DefaultTheme" >
<intent-filter>
<action android:name="android.intent.action.DISPLAY_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="acr.browser.lightning.PrivacySettingsActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/settings_privacy"
android:theme="@style/DefaultTheme" >
<intent-filter>
<action android:name="android.intent.action.PRIVACY_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="acr.browser.lightning.AdvancedSettingsActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/settings_advanced"
android:theme="@style/DefaultTheme" >
<intent-filter>
<action android:name="android.intent.action.ADVANCED_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="acr.browser.lightning.AboutSettingsActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/settings_about"
android:theme="@style/DefaultTheme" >
<intent-filter>
<action android:name="android.intent.action.ABOUT_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="acr.browser.lightning.IncognitoActivity"
android:alwaysRetainTaskState="true"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/DarkTheme"
android:windowSoftInputMode="stateHidden" >
<intent-filter>
<action android:name="android.intent.action.INCOGNITO" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="acr.browser.lightning.LicenseActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/licenses"
android:theme="@style/DefaultTheme" >
<intent-filter>
<action android:name="android.intent.action.LICENSE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="acr.browser.lightning.BookmarkActivity"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/bookmark_settings"
android:theme="@style/DefaultTheme" >
<intent-filter>
<action android:name="android.intent.action.BOOKMARK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>
+19
Ver Arquivo
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="Lightning-Browser" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="java-gradle" name="Java-Gradle">
<configuration>
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
<option name="BUILDABLE" value="false" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+22 -10
Ver Arquivo
@@ -4,10 +4,17 @@
####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)
####Master Branch: [![Build Status](https://travis-ci.org/anthonycr/Lightning-Browser.svg?branch=master)](https://travis-ci.org/anthonycr/Lightning-Browser)
####Dev Branch: [![Build Status](https://travis-ci.org/anthonycr/Lightning-Browser.svg?branch=dev)](https://travis-ci.org/anthonycr/Lightning-Browser)
* [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)
####Dev Branch
* [![Build Status](https://travis-ci.org/anthonycr/Lightning-Browser.svg?branch=dev)](https://travis-ci.org/anthonycr/Lightning-Browser)
####Features
* Bookmarks
@@ -18,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
@@ -38,16 +43,23 @@
* ````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
* Please contribute code back if you can. The code isn't perfect.
* 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):
````
+3
Ver Arquivo
@@ -0,0 +1,3 @@
/build
*.apk
manifest-merger-release-report.txt
+107
Ver Arquivo
@@ -0,0 +1,107 @@
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
apply plugin: 'com.getkeepsafe.dexcount'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 14
targetSdkVersion 23
versionName "4.3.3"
generatedDensities = []
}
aaptOptions {
additionalParameters "--no-version-vectors"
}
sourceSets {
lightningPlus.setRoot('src/LightningPlus')
lightningLite.setRoot('src/LightningLite')
}
buildTypes {
debug {
minifyEnabled false
shrinkResources false
proguardFiles 'proguard-project.txt'
}
release {
minifyEnabled true
shrinkResources true
proguardFiles 'proguard-project.txt'
}
}
productFlavors {
lightningPlus {
buildConfigField "boolean", "FULL_VERSION", "true"
applicationId "acr.browser.lightning"
versionCode 88
}
lightningLite {
buildConfigField "boolean", "FULL_VERSION", "false"
applicationId "acr.browser.barebones"
versionCode 90
}
}
lintOptions {
abortOnError true
}
packagingOptions {
exclude '.readme'
}
}
dexcount {
includeClasses = false
includeFieldCount = false
printAsTree = true
orderByMethodCount = true
verbose = false
}
dependencies {
// 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'
}
@@ -34,12 +34,44 @@
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class com.android.vending.licensing.ILicensingService
-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(...);
public static *** w(...);
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 *;
}
# Without this rule, openFileChooser does not get called on KitKat
-keep class acr.browser.lightning.LightningView$LightningChromeClient {
void openFileChooser(android.webkit.ValueCallback);
void openFileChooser(android.webkit.ValueCallback, java.lang.String);
void openFileChooser(android.webkit.ValueCallback, java.lang.String, java.lang.String);
-keep class acr.browser.lightning.view.LightningView$LightningChromeClient {
void openFileChooser(android.webkit.ValueCallback);
void openFileChooser(android.webkit.ValueCallback, java.lang.String);
void openFileChooser(android.webkit.ValueCallback, java.lang.String, java.lang.String);
}
-keepclasseswithmembernames class * {
@@ -66,3 +98,16 @@
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# The support library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version. We know about them, and they are safe.
-dontwarn android.support.**
# The I2P Java API bundled inside the I2P Android client library contains
# references to javax.naming classes that Android doesn't have. But those
# classes are never used on Android, and it is safe to ignore the warnings.
-dontwarn net.i2p.crypto.CertUtil
-dontwarn org.apache.http.conn.ssl.DefaultHostnameVerifier
-dontwarn org.apache.http.HttpHost
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
+153
Ver Arquivo
@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2014 A.C.R. Development -->
<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="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"/>
<uses-feature
android:name="android.hardware.location"
android:required="false"/>
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false"/>
<application
android:name=".app.BrowserApp"
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
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:windowSoftInputMode="adjustResize">
<intent-filter>
<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"/>
</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="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"/>
<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"/>
</intent-filter>
<!-- For viewing saved web archives. -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<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"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.WEB_SEARCH"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<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">
<intent-filter>
<action android:name="android.intent.action.SETTINGS"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".activity.IncognitoActivity"
android:alwaysRetainTaskState="true"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/Theme.DarkTheme"
android:windowSoftInputMode="stateHidden|adjustResize">
<intent-filter>
<action android:name="android.intent.action.INCOGNITO"/>
<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">
<intent-filter>
<action android:name="android.intent.action.READING"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
</application>
</manifest>
Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 89 KiB

Antes

Largura:  |  Altura:  |  Tamanho: 3.4 KiB

Depois

Largura:  |  Altura:  |  Tamanho: 3.4 KiB

Antes

Largura:  |  Altura:  |  Tamanho: 12 KiB

Depois

Largura:  |  Altura:  |  Tamanho: 12 KiB

Antes

Largura:  |  Altura:  |  Tamanho: 20 KiB

Depois

Largura:  |  Altura:  |  Tamanho: 20 KiB

Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 23 KiB

Ver Arquivo

Antes

Largura:  |  Altura:  |  Tamanho: 21 KiB

Depois

Largura:  |  Altura:  |  Tamanho: 21 KiB

Antes

Largura:  |  Altura:  |  Tamanho: 14 KiB

Depois

Largura:  |  Altura:  |  Tamanho: 14 KiB

Antes

Largura:  |  Altura:  |  Tamanho: 44 KiB

Depois

Largura:  |  Altura:  |  Tamanho: 44 KiB

Antes

Largura:  |  Altura:  |  Tamanho: 18 KiB

Depois

Largura:  |  Altura:  |  Tamanho: 18 KiB

@@ -0,0 +1,125 @@
package acr.browser.lightning.activity;
import android.content.res.Configuration;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.Toolbar;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import acr.browser.lightning.R;
/**
* A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
* to be used with AppCompat.
* <p/>
* This technique can be used with an {@link android.app.Activity} class, not just
* {@link android.preference.PreferenceActivity}.
*/
public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
private AppCompatDelegate mDelegate;
@Override
protected void onCreate(Bundle savedInstanceState) {
overridePendingTransition(R.anim.slide_in_from_right, R.anim.fade_out_scale);
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
getDelegate().onPostCreate(savedInstanceState);
}
ActionBar getSupportActionBar() {
return getDelegate().getSupportActionBar();
}
void setSupportActionBar(@Nullable Toolbar toolbar) {
getDelegate().setSupportActionBar(toolbar);
}
@NonNull
@Override
public MenuInflater getMenuInflater() {
return getDelegate().getMenuInflater();
}
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
@Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().setContentView(view, params);
}
@Override
public void addContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().addContentView(view, params);
}
@Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
@Override
protected void onTitleChanged(CharSequence title, int color) {
super.onTitleChanged(title, color);
getDelegate().setTitle(title);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
getDelegate().onConfigurationChanged(newConfig);
}
@Override
protected void onPause() {
super.onPause();
if (isFinishing()) {
overridePendingTransition(R.anim.fade_in_scale, R.anim.slide_out_to_right);
}
}
@Override
protected void onStop() {
super.onStop();
getDelegate().onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
getDelegate().onDestroy();
}
public void invalidateOptionsMenu() {
getDelegate().invalidateOptionsMenu();
}
private AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, null);
}
return mDelegate;
}
}
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
@@ -0,0 +1,71 @@
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.react.Action;
import acr.browser.lightning.react.Observable;
import acr.browser.lightning.react.Subscriber;
@SuppressWarnings("deprecation")
public class IncognitoActivity extends BrowserActivity {
@Override
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
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.incognito, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
protected void onNewIntent(Intent intent) {
// handleNewIntent(intent);
super.onNewIntent(intent);
}
@Override
protected void onPause() {
super.onPause();
// saveOpenTabs();
}
@Override
public void updateHistory(@Nullable String title, @NonNull String url) {
// addItemToHistory(title, url);
}
@Override
public boolean isIncognito() {
return true;
}
@Override
public void closeActivity() {
closeDrawers(new Runnable() {
@Override
public void run() {
closeBrowser();
}
});
}
}
@@ -0,0 +1,78 @@
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.react.Action;
import acr.browser.lightning.react.Observable;
import acr.browser.lightning.react.Subscriber;
@SuppressWarnings("deprecation")
public class MainActivity extends BrowserActivity {
@Override
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
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
protected void onNewIntent(Intent intent) {
if (isPanicTrigger(intent)) {
panicClean();
} else {
handleNewIntent(intent);
super.onNewIntent(intent);
}
}
@Override
protected void onPause() {
super.onPause();
saveOpenTabs();
}
@Override
public void updateHistory(@Nullable String title, @NonNull String url) {
addItemToHistory(title, url);
}
@Override
public boolean isIncognito() {
return false;
}
@Override
public void closeActivity() {
closeDrawers(new Runnable() {
@Override
public void run() {
performExitCleanUp();
moveTaskToBack(true);
}
});
}
}
@@ -0,0 +1,332 @@
package acr.browser.lightning.activity;
import android.animation.ObjectAnimator;
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.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;
import android.view.View;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
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 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 int mTextSize;
private ProgressDialog mProgressDialog;
private Subscription mPageLoaderSubscription;
private static final float XXLARGE = 30.0f;
private static final float XLARGE = 26.0f;
private static final float LARGE = 22.0f;
private static final float MEDIUM = 18.0f;
private static final float SMALL = 14.0f;
private static final float XSMALL = 10.0f;
@Override
protected void onCreate(Bundle savedInstanceState) {
BrowserApp.getAppComponent().inject(this);
overridePendingTransition(R.anim.slide_in_from_right, R.anim.fade_out_scale);
mInvert = mPreferences.getInvertColors();
final int color;
if (mInvert) {
setTheme(R.style.Theme_SettingsTheme_Dark);
color = ThemeUtils.getPrimaryColorDark(this);
getWindow().setBackgroundDrawable(new ColorDrawable(color));
} else {
setTheme(R.style.Theme_SettingsTheme);
color = ThemeUtils.getPrimaryColor(this);
getWindow().setBackgroundDrawable(new ColorDrawable(color));
}
super.onCreate(savedInstanceState);
setContentView(R.layout.reading_view);
ButterKnife.bind(this);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mTextSize = mPreferences.getReadingTextSize();
mBody.setTextSize(getTextSize(mTextSize));
mTitle.setText(getString(R.string.untitled));
mBody.setText(getString(R.string.loading));
mTitle.setVisibility(View.INVISIBLE);
mBody.setVisibility(View.INVISIBLE);
Intent intent = getIntent();
if (!loadPage(intent)) {
setText(getString(R.string.untitled), getString(R.string.loading_failed));
}
}
private static float getTextSize(int size) {
switch (size) {
case 0:
return XSMALL;
case 1:
return SMALL;
case 2:
return MEDIUM;
case 3:
return LARGE;
case 4:
return XLARGE;
case 5:
return XXLARGE;
default:
return MEDIUM;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.reading, menu);
MenuItem invert = menu.findItem(R.id.invert_item);
MenuItem textSize = menu.findItem(R.id.text_size_item);
int iconColor = mInvert ? ThemeUtils.getIconDarkThemeColor(this) : ThemeUtils.getIconLightThemeColor(this);
if (invert != null && invert.getIcon() != null)
invert.getIcon().setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
if (textSize != null && textSize.getIcon() != null)
textSize.getIcon().setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
return super.onCreateOptionsMenu(menu);
}
private boolean loadPage(Intent intent) {
if (intent == null) {
return false;
}
mUrl = intent.getStringExtra(Constants.LOAD_READING_URL);
if (mUrl == null) {
return false;
}
if (getSupportActionBar() != null)
getSupportActionBar().setTitle(Utils.getDomainName(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 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();
}
});
}
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;
}
@NonNull
public String getTitle() {
return mTitleText;
}
@NonNull
public String getBody() {
return mBodyText;
}
}
private void setText(String title, String body) {
if (mTitle == null || mBody == null)
return;
if (mTitle.getVisibility() == View.INVISIBLE) {
mTitle.setAlpha(0.0f);
mTitle.setVisibility(View.VISIBLE);
mTitle.setText(title);
ObjectAnimator animator = ObjectAnimator.ofFloat(mTitle, "alpha", 1.0f);
animator.setDuration(300);
animator.start();
} else {
mTitle.setText(title);
}
if (mBody.getVisibility() == View.INVISIBLE) {
mBody.setAlpha(0.0f);
mBody.setVisibility(View.VISIBLE);
mBody.setText(body);
ObjectAnimator animator = ObjectAnimator.ofFloat(mBody, "alpha", 1.0f);
animator.setDuration(300);
animator.start();
} else {
mBody.setText(body);
}
}
@Override
protected void onDestroy() {
mPageLoaderSubscription.unsubscribe();
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
mProgressDialog = null;
}
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()) {
case R.id.invert_item:
mPreferences.setInvertColors(!mInvert);
Intent read = new Intent(this, ReadingActivity.class);
read.putExtra(Constants.LOAD_READING_URL, mUrl);
startActivity(read);
finish();
break;
case R.id.text_size_item:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
LayoutInflater inflater = this.getLayoutInflater();
View view = inflater.inflate(R.layout.seek_layout, null);
final SeekBar bar = (SeekBar) view.findViewById(R.id.text_size_seekbar);
bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar view, int size, boolean user) {
mBody.setTextSize(getTextSize(size));
}
@Override
public void onStartTrackingTouch(SeekBar arg0) {
}
@Override
public void onStopTrackingTouch(SeekBar arg0) {
}
});
bar.setMax(5);
bar.setProgress(mTextSize);
builder.setView(view);
builder.setTitle(R.string.size);
builder.setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
mTextSize = bar.getProgress();
mBody.setTextSize(getTextSize(mTextSize));
mPreferences.setReadingTextSize(bar.getProgress());
}
});
builder.show();
break;
default:
finish();
break;
}
return super.onOptionsItemSelected(item);
}
}
@@ -0,0 +1,86 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.activity;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
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.app.BrowserApp;
public class SettingsActivity extends ThemableSettingsActivity {
private static final List<String> mFragments = new ArrayList<>(7);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// this is a workaround for the Toolbar in PreferenceActitivty
ViewGroup root = (ViewGroup) findViewById(android.R.id.content);
LinearLayout content = (LinearLayout) root.getChildAt(0);
LinearLayout toolbarContainer = (LinearLayout) View.inflate(this, R.layout.toolbar_settings, null);
root.removeAllViews();
toolbarContainer.addView(content);
root.addView(toolbarContainer);
// now we can set the Toolbar using AppCompatPreferenceActivity
Toolbar toolbar = (Toolbar) toolbarContainer.findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.preferences_headers, target);
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 mFragments.contains(fragmentName);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
finish();
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
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;
}
}
}
@@ -0,0 +1,79 @@
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) {
BrowserApp.getAppComponent().inject(this);
mTheme = mPreferences.getUseTheme();
mShowTabsInDrawer = mPreferences.getShowTabsInDrawer(!isTablet());
// set the theme
if (mTheme == 1) {
setTheme(R.style.Theme_DarkTheme);
} else if (mTheme == 2) {
setTheme(R.style.Theme_BlackTheme);
}
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();
mShouldRunOnResumeActions = true;
int theme = mPreferences.getUseTheme();
boolean drawerTabs = mPreferences.getShowTabsInDrawer(!isTablet());
if (theme != mTheme || mShowTabsInDrawer != drawerTabs) {
restart();
}
}
boolean isTablet() {
return (getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE;
}
private void restart() {
finish();
startActivity(new Intent(this, getClass()));
}
}
@@ -0,0 +1,49 @@
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;
public abstract class ThemableSettingsActivity extends AppCompatPreferenceActivity {
private int mTheme;
@Inject PreferenceManager mPreferenceManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
BrowserApp.getAppComponent().inject(this);
mTheme = mPreferenceManager.getUseTheme();
// set the theme
if (mTheme == 0) {
setTheme(R.style.Theme_SettingsTheme);
this.getWindow().setBackgroundDrawable(new ColorDrawable(ThemeUtils.getPrimaryColor(this)));
} else if (mTheme == 1) {
setTheme(R.style.Theme_SettingsTheme_Dark);
this.getWindow().setBackgroundDrawable(new ColorDrawable(ThemeUtils.getPrimaryColorDark(this)));
} else if (mTheme == 2) {
setTheme(R.style.Theme_SettingsTheme_Black);
this.getWindow().setBackgroundDrawable(new ColorDrawable(ThemeUtils.getPrimaryColorDark(this)));
}
super.onCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
if (mPreferenceManager.getUseTheme() != mTheme) {
restart();
}
}
private void restart() {
recreate();
}
}
@@ -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 {
}
}
@@ -0,0 +1,162 @@
/*
* Copyright 2014 A.C.R. Development
*/
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.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 final class BookmarkPage extends AsyncTask<Void, Void, Void> {
/**
* The bookmark page standard suffix
*/
public static final String FILENAME = "bookmarks.html";
private static final String HEADING_1 = "<!DOCTYPE html><html xmlns=http://www.w3.org/1999/xhtml>\n" +
"<head>\n" +
"<meta content=en-us http-equiv=Content-Language />\n" +
"<meta content='text/html; charset=utf-8' http-equiv=Content-Type />\n" +
"<meta name=viewport content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'>\n" +
"<title>";
private static final String HEADING_2 = "</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>";
private static final String PART1 = "<div class=box><a href='";
private static final String PART2 = "'></a>\n" +
"<div class=margin>\n" +
"<div class=box-content>\n" +
"<p class=ellipses>\n" +
"<img src='";
private static final String PART3 = "https://www.google.com/s2/favicons?domain=";
private static final String PART4 = "' />";
private static final String PART5 = "</p></div></div></div>";
private static final String END = "</div></body></html>";
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);
}
}
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(PART1);
if (item.isFolder()) {
final File folderPage = new File(mFilesDir, item.getTitle() + '-' + FILENAME);
bookmarkBuilder.append(Constants.FILE).append(folderPage);
bookmarkBuilder.append(PART2);
bookmarkBuilder.append(folderIconPath);
buildBookmarkPage(item.getTitle(), manager);
} else {
bookmarkBuilder.append(item.getUrl());
bookmarkBuilder.append(PART2).append(PART3);
bookmarkBuilder.append(item.getUrl());
}
bookmarkBuilder.append(PART4);
bookmarkBuilder.append(item.getTitle());
bookmarkBuilder.append(PART5);
}
bookmarkBuilder.append(END);
FileWriter bookWriter = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
bookWriter = new FileWriter(bookmarkWebPage, false);
bookWriter.write(bookmarkBuilder.toString());
} catch (IOException e) {
e.printStackTrace();
} finally {
Utils.close(bookWriter);
}
}
public void load() {
executeOnExecutor(BrowserApp.getIOThread());
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.constant;
import acr.browser.lightning.BuildConfig;
public final class Constants {
private Constants() {
}
public static final boolean FULL_VERSION = BuildConfig.FULL_VERSION;
public static final String DESKTOP_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36";
public static final String MOBILE_USER_AGENT = "Mozilla/5.0 (Linux; U; Android 4.4; en-us; Nexus 4 Build/JOP24G) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30";
public static final String YAHOO_SEARCH = "https://search.yahoo.com/search?p=";
public static final String GOOGLE_SEARCH = "https://www.google.com/search?client=lightning&ie=UTF-8&oe=UTF-8&q=";
public static final String BING_SEARCH = "https://www.bing.com/search?q=";
public static final String DUCK_SEARCH = "https://duckduckgo.com/?t=lightning&q=";
public static final String DUCK_LITE_SEARCH = "https://duckduckgo.com/lite/?t=lightning&q=";
public static final String STARTPAGE_MOBILE_SEARCH = "https://startpage.com/do/m/mobilesearch?language=english&query=";
public static final String STARTPAGE_SEARCH = "https://startpage.com/do/search?language=english&query=";
public static final String ASK_SEARCH = "http://www.ask.com/web?qsrc=0&o=0&l=dir&qo=lightningBrowser&q=";
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 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";
public static final String SEPARATOR = "\\|\\$\\|SEPARATOR\\|\\$\\|";
public static final String HTTP = "http://";
public static final String HTTPS = "https://";
public static final String FILE = "file://";
public static final String FOLDER = "folder://";
public static final String TAG = "Lightning";
// These should match the order of @array/proxy_choices_array
public static final int NO_PROXY = 0;
public static final int PROXY_ORBOT = 1;
public static final int PROXY_I2P = 2;
public static final int PROXY_MANUAL = 3;
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";
}
@@ -0,0 +1,123 @@
/*
* Copyright 2014 A.C.R. Development
*/
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 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 extends AsyncTask<Void, Void, Void> {
public static final String FILENAME = "history.html";
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=\"";
private static final String PART2 = "\"></a><p class=\"black\">";
private static final String PART3 = "</p><p class=\"font\">";
private static final String PART4 = "</p></div></div>";
private static final String END = "</div></body></html>";
@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(PART1);
historyBuilder.append(helper.getUrl());
historyBuilder.append(PART2);
historyBuilder.append(helper.getTitle());
historyBuilder.append(PART3);
historyBuilder.append(helper.getUrl());
historyBuilder.append(PART4);
}
historyBuilder.append(END);
File historyWebPage = new File(mApp.getFilesDir(), FILENAME);
FileWriter historyWriter = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
historyWriter = new FileWriter(historyWebPage, false);
historyWriter.write(historyBuilder.toString());
} catch (IOException e) {
e.printStackTrace();
} finally {
Utils.close(historyWriter);
}
return Constants.FILE + historyWebPage;
}
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();
}
}
}
@@ -0,0 +1,196 @@
/*
* Copyright 2014 A.C.R. Development
*/
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 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;
import acr.browser.lightning.view.LightningView;
public class StartPage extends AsyncTask<Void, Void, Void> {
public static final String FILENAME = "homepage.html";
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>";
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;}"
+ "span { display: block; overflow: hidden; padding-left:5px;vertical-align:middle;}"
+ ".search_bar{display:table;vertical-align:middle;width:90%;height:35px;max-width:500px;margin:0 auto;background-color:#fff;box-shadow: 0px 2px 3px rgba( 0, 0, 0, 0.25 );"
+ "font-family: Arial;color: #444;-moz-border-radius: 2px;-webkit-border-radius: 2px;border-radius: 2px;}"
+ "#search_submit{outline:none;height:37px;float:right;color:#404040;font-size:16px;font-weight:bold;border:none;"
+ "background-color:transparent;}.outer { display: table; position: absolute; height: 100%; width: 100%;}"
+ ".middle { display: table-cell; vertical-align: middle;}.inner { margin-left: auto; margin-right: auto; "
+ "margin-bottom:10%; width: 100%;}img.smaller{width:50%;max-width:300px;}"
+ ".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 3px rgba( 0, 0, 0, 0.1 );font-family: Arial;color: #444;"
+ "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=\"";
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 = \"";
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
* when it finishes building.
*
* @return the URL to load
*/
@NonNull
private String getHomepage() {
StringBuilder homepageBuilder = new StringBuilder(HEAD_1 + mTitle + HEAD_2);
String icon;
String searchUrl;
switch (mPreferenceManager.getSearchChoice()) {
case 0:
// CUSTOM SEARCH
icon = "file:///android_asset/lightning.png";
searchUrl = mPreferenceManager.getSearchUrl();
break;
case 1:
// GOOGLE_SEARCH;
icon = "file:///android_asset/google.png";
// "https://www.google.com/images/srpr/logo11w.png";
searchUrl = Constants.GOOGLE_SEARCH;
break;
case 2:
// ANDROID SEARCH;
icon = "file:///android_asset/ask.png";
searchUrl = Constants.ASK_SEARCH;
break;
case 3:
// BING_SEARCH;
icon = "file:///android_asset/bing.png";
// "http://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/Bing_logo_%282013%29.svg/500px-Bing_logo_%282013%29.svg.png";
searchUrl = Constants.BING_SEARCH;
break;
case 4:
// YAHOO_SEARCH;
icon = "file:///android_asset/yahoo.png";
// "http://upload.wikimedia.org/wikipedia/commons/thumb/2/24/Yahoo%21_logo.svg/799px-Yahoo%21_logo.svg.png";
searchUrl = Constants.YAHOO_SEARCH;
break;
case 5:
// STARTPAGE_SEARCH;
icon = "file:///android_asset/png";
// "https://com/graphics/startp_logo.gif";
searchUrl = Constants.STARTPAGE_SEARCH;
break;
case 6:
// STARTPAGE_MOBILE
icon = "file:///android_asset/png";
// "https://com/graphics/startp_logo.gif";
searchUrl = Constants.STARTPAGE_MOBILE_SEARCH;
break;
case 7:
// DUCK_SEARCH;
icon = "file:///android_asset/duckduckgo.png";
// "https://duckduckgo.com/assets/logo_homepage.normal.v101.png";
searchUrl = Constants.DUCK_SEARCH;
break;
case 8:
// DUCK_LITE_SEARCH;
icon = "file:///android_asset/duckduckgo.png";
// "https://duckduckgo.com/assets/logo_homepage.normal.v101.png";
searchUrl = Constants.DUCK_LITE_SEARCH;
break;
case 9:
// BAIDU_SEARCH;
icon = "file:///android_asset/baidu.png";
// "http://www.baidu.com/img/bdlogo.gif";
searchUrl = Constants.BAIDU_SEARCH;
break;
case 10:
// YANDEX_SEARCH;
icon = "file:///android_asset/yandex.png";
// "http://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Yandex.svg/600px-Yandex.svg.png";
searchUrl = Constants.YANDEX_SEARCH;
break;
default:
// DEFAULT GOOGLE_SEARCH;
icon = "file:///android_asset/google.png";
searchUrl = Constants.GOOGLE_SEARCH;
break;
}
homepageBuilder.append(icon);
homepageBuilder.append(MIDDLE);
homepageBuilder.append(searchUrl);
homepageBuilder.append(END);
File homepage = new File(mApp.getFilesDir(), FILENAME);
FileWriter hWriter = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
hWriter = new FileWriter(homepage, false);
hWriter.write(homepageBuilder.toString());
} catch (IOException e) {
e.printStackTrace();
} finally {
Utils.close(hWriter);
}
return Constants.FILE + homepage;
}
public void load() {
executeOnExecutor(BrowserApp.getIOThread());
}
}
@@ -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();
}
}
}
@@ -0,0 +1,558 @@
package acr.browser.lightning.database;
import android.app.Activity;
import android.content.Context;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.inject.Inject;
import javax.inject.Singleton;
import acr.browser.lightning.R;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.utils.Utils;
@Singleton
public class BookmarkManager {
private static final String TAG = BookmarkManager.class.getSimpleName();
private static final String TITLE = "title";
private static final String URL = "url";
private static final String FOLDER = "folder";
private static final String ORDER = "order";
private static final String FILE_BOOKMARKS = "bookmarks.dat";
@NonNull private final String DEFAULT_BOOKMARK_TITLE;
private Map<String, HistoryItem> mBookmarksMap;
@NonNull private String mCurrentFolder = "";
@NonNull private final ExecutorService mExecutor;
private File mFilesDir;
@Inject
public BookmarkManager(@NonNull Context context) {
mExecutor = Executors.newSingleThreadExecutor();
DEFAULT_BOOKMARK_TITLE = context.getString(R.string.untitled);
mExecutor.execute(new BookmarkInitializer(context));
}
/**
* Look for bookmark using the url
*
* @param url the lookup url
* @return the bookmark as an {@link HistoryItem} or null
*/
@Nullable
public HistoryItem findBookmarkForUrl(final String url) {
return mBookmarksMap.get(url);
}
/**
* Initialize the BookmarkManager, it's a one-time operation and will be executed asynchronously.
* When done, mReady flag will been set to true.
*/
private class BookmarkInitializer implements Runnable {
private final Context mContext;
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(@NonNull HistoryItem item) {
final String url = item.getUrl();
if (mBookmarksMap.containsKey(url)) {
return false;
}
mBookmarksMap.put(url, item);
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
return true;
}
/**
* This method adds the list of HistoryItems to permanent bookmark storage
*
* @param list the list of HistoryItems to add to bookmarks
*/
public synchronized void addBookmarkList(@Nullable List<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())));
}
/**
* This method deletes the bookmark with the given url. It returns
* true if the deletion was successful.
*
* @param deleteItem the bookmark item to delete
*/
public synchronized boolean deleteBookmark(@Nullable HistoryItem deleteItem) {
if (deleteItem == null || deleteItem.isFolder()) {
return false;
}
mBookmarksMap.remove(deleteItem.getUrl());
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
return true;
}
/**
* renames a folder and moves all it's contents to that folder
*
* @param oldName the folder to be renamed
* @param newName the new name of the folder
*/
public synchronized void renameFolder(@NonNull String oldName, @NonNull String newName) {
if (newName.isEmpty()) {
return;
}
for (HistoryItem item : mBookmarksMap.values()) {
if (item.getFolder().equals(oldName)) {
item.setFolder(newName);
} else if (item.isFolder() && item.getTitle().equals(oldName)) {
item.setTitle(newName);
item.setUrl(Constants.FOLDER + newName);
}
}
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
}
/**
* Delete the folder and move all bookmarks to the top level
*
* @param name the name of the folder to be deleted
*/
public synchronized void deleteFolder(@NonNull String name) {
final Map<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);
}
}
mBookmarksMap = bookmarks;
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
}
/**
* This method deletes ALL bookmarks created
* by the user. Use this method carefully and
* do not use it without explicit user consent.
*/
public synchronized void deleteAllBookmarks() {
mBookmarksMap = new HashMap<>();
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
}
/**
* This method edits a particular bookmark in the bookmark database
*
* @param oldItem This is the old item that you wish to edit
* @param newItem This is the new item that will overwrite the old item
*/
public synchronized void editBookmark(@Nullable HistoryItem oldItem, @Nullable HistoryItem newItem) {
if (oldItem == null || newItem == null || oldItem.isFolder()) {
return;
}
if (newItem.getUrl().isEmpty()) {
deleteBookmark(oldItem);
return;
}
if (newItem.getTitle().isEmpty()) {
newItem.setTitle(DEFAULT_BOOKMARK_TITLE);
}
final String oldUrl = oldItem.getUrl();
final String newUrl = newItem.getUrl();
if (!oldUrl.equals(newUrl)) {
// The url has been changed, remove the old one
mBookmarksMap.remove(oldUrl);
}
mBookmarksMap.put(newUrl, newItem);
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
}
/**
* This method exports the stored bookmarks to a text file in the device's
* external download directory
*/
public synchronized void exportBookmarks(@NonNull Activity activity) {
List<HistoryItem> bookmarkList = getAllBookmarks(true);
File bookmarksExport = new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"BookmarksExport.txt");
int counter = 0;
while (bookmarksExport.exists()) {
counter++;
bookmarksExport = new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"BookmarksExport-" + counter + ".txt");
}
BufferedWriter bookmarkWriter = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
bookmarkWriter = new BufferedWriter(new FileWriter(bookmarksExport,
false));
JSONObject object = new JSONObject();
for (HistoryItem item : bookmarkList) {
object.put(TITLE, item.getTitle());
object.put(URL, item.getUrl());
object.put(FOLDER, item.getFolder());
object.put(ORDER, item.getOrder());
bookmarkWriter.write(object.toString());
bookmarkWriter.newLine();
}
Utils.showSnackbar(activity, activity.getString(R.string.bookmark_export_path)
+ ' ' + bookmarksExport.getPath());
} catch (@NonNull IOException | JSONException e) {
e.printStackTrace();
} finally {
Utils.close(bookmarkWriter);
}
}
/**
* This method returns a list of ALL stored bookmarks.
* This is a disk-bound operation and should not be
* done very frequently.
*
* @param sort force to sort the returned bookmarkList
* @return returns a list of bookmarks that can be sorted
*/
@NonNull
public synchronized List<HistoryItem> getAllBookmarks(boolean sort) {
final List<HistoryItem> bookmarks = new ArrayList<>(mBookmarksMap.values());
if (sort) {
Collections.sort(bookmarks, new SortIgnoreCase());
}
return bookmarks;
}
/**
* This method returns a list of bookmarks and folders located in the specified folder.
* This method should generally be used by the UI when it needs a list to display to the
* user as it returns a subset of all bookmarks and includes folders as well which are
* really 'fake' bookmarks.
*
* @param folder the name of the folder to retrieve bookmarks from
* @return a list of bookmarks found in that folder
*/
@NonNull
public synchronized List<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 (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());
}
return bookmarks;
}
/**
* Tells you if you are at the root folder or in a subfolder
*
* @return returns true if you are in the root folder
*/
public boolean isRootFolder() {
return mCurrentFolder.isEmpty();
}
/**
* Returns the current folder
*
* @return the current folder
*/
@Nullable
public String getCurrentFolder() {
return mCurrentFolder;
}
/**
* This method returns a list of all folders.
* Folders cannot be empty as they are generated from
* the list of bookmarks that have non-empty folder fields.
*
* @return a list of all folders
*/
@NonNull
private synchronized List<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);
}
}
final List<HistoryItem> result = new ArrayList<>(folders.values());
if (sort) {
Collections.sort(result, new SortIgnoreCase());
}
return result;
}
/**
* returns a list of folder titles that can be used for suggestions in a
* simple list adapter
*
* @return a list of folder title strings
*/
@NonNull
public synchronized List<String> getFolderTitles() {
final Set<String> folders = new HashSet<>();
for (HistoryItem item : mBookmarksMap.values()) {
final String folderName = item.getFolder();
if (!folderName.isEmpty()) {
folders.add(folderName);
}
}
return new ArrayList<>(folders);
}
/**
* This method imports the bookmarks from a backup file that is located on
* external storage
*
* @param file the file to attempt to import bookmarks from
*/
public synchronized void importBookmarksFromFile(@Nullable File file, @NonNull Activity activity) {
if (file == null) {
return;
}
List<HistoryItem> list = new ArrayList<>();
BufferedReader bookmarksReader = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
bookmarksReader = new BufferedReader(new FileReader(file));
String line;
int number = 0;
while ((line = bookmarksReader.readLine()) != null) {
JSONObject object = new JSONObject(line);
HistoryItem item = new HistoryItem();
item.setTitle(object.getString(TITLE));
item.setUrl(object.getString(URL));
item.setFolder(object.getString(FOLDER));
item.setOrder(object.getInt(ORDER));
list.add(item);
number++;
}
addBookmarkList(list);
String message = activity.getResources().getString(R.string.message_import);
Utils.showSnackbar(activity, number + " " + message);
} catch (@NonNull IOException | JSONException e) {
e.printStackTrace();
Utils.createInformativeDialog(activity, R.string.title_error, R.string.import_bookmark_error);
} finally {
Utils.close(bookmarksReader);
}
}
/**
* This class sorts bookmarks alphabetically, with folders coming after bookmarks
*/
private static class SortIgnoreCase implements Comparator<HistoryItem> {
public int compare(@Nullable HistoryItem o1, @Nullable HistoryItem o2) {
if (o1 == null || o2 == null) {
return 0;
}
if (o1.isFolder() == o2.isFolder()) {
return o1.getTitle().toLowerCase(Locale.getDefault())
.compareTo(o2.getTitle().toLowerCase(Locale.getDefault()));
} else {
return o1.isFolder() ? 1 : -1;
}
}
}
}
@@ -0,0 +1,227 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
@Singleton
public class HistoryDatabase extends SQLiteOpenHelper {
// All Static variables
// Database Version
private static final int DATABASE_VERSION = 2;
// Database Name
private static final String DATABASE_NAME = "historyManager";
// HistoryItems table name
private static final String TABLE_HISTORY = "history";
// HistoryItems Table Columns names
private static final String KEY_ID = "id";
private static final String KEY_URL = "url";
private static final String KEY_TITLE = "title";
private static final String KEY_TIME_VISITED = "time";
@Nullable private SQLiteDatabase mDatabase;
@Inject
public HistoryDatabase(@NonNull Context context) {
super(context.getApplicationContext(), DATABASE_NAME, null, DATABASE_VERSION);
initialize();
}
private void initialize() {
BrowserApp.getTaskThread().execute(new Runnable() {
@Override
public void run() {
synchronized (HistoryDatabase.this) {
mDatabase = HistoryDatabase.this.getWritableDatabase();
}
}
});
}
// Creating Tables
@Override
public void onCreate(@NonNull SQLiteDatabase db) {
String CREATE_HISTORY_TABLE = "CREATE TABLE " + TABLE_HISTORY + '(' + KEY_ID
+ " INTEGER PRIMARY KEY," + KEY_URL + " TEXT," + KEY_TITLE + " TEXT,"
+ KEY_TIME_VISITED + " INTEGER" + ')';
db.execSQL(CREATE_HISTORY_TABLE);
}
// Upgrading database
@Override
public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) {
// Drop older table if it exists
db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY);
// Create tables again
onCreate(db);
}
public synchronized void deleteHistory() {
mDatabase = openIfNecessary();
mDatabase.delete(TABLE_HISTORY, null, null);
mDatabase.close();
mDatabase = this.getWritableDatabase();
}
@Override
public synchronized void close() {
if (mDatabase != null) {
mDatabase.close();
mDatabase = null;
}
super.close();
}
@NonNull
private SQLiteDatabase openIfNecessary() {
if (mDatabase == null || !mDatabase.isOpen()) {
mDatabase = this.getWritableDatabase();
}
return mDatabase;
}
public synchronized void deleteHistoryItem(@NonNull String url) {
mDatabase = openIfNecessary();
mDatabase.delete(TABLE_HISTORY, KEY_URL + " = ?", new String[]{url});
}
public synchronized void visitHistoryItem(@NonNull String url, @Nullable String title) {
mDatabase = openIfNecessary();
ContentValues values = new ContentValues();
values.put(KEY_TITLE, title == null ? "" : title);
values.put(KEY_TIME_VISITED, System.currentTimeMillis());
Cursor q = mDatabase.query(false, TABLE_HISTORY, new String[]{KEY_URL},
KEY_URL + " = ?", new String[]{url}, null, null, null, "1");
if (q.getCount() > 0) {
mDatabase.update(TABLE_HISTORY, values, KEY_URL + " = ?", new String[]{url});
} else {
addHistoryItem(new HistoryItem(url, title == null ? "" : title));
}
q.close();
}
private synchronized void addHistoryItem(@NonNull HistoryItem item) {
mDatabase = openIfNecessary();
ContentValues values = new ContentValues();
values.put(KEY_URL, item.getUrl());
values.put(KEY_TITLE, item.getTitle());
values.put(KEY_TIME_VISITED, System.currentTimeMillis());
mDatabase.insert(TABLE_HISTORY, null, values);
}
@Nullable
synchronized String getHistoryItem(@NonNull String url) {
mDatabase = openIfNecessary();
Cursor cursor = mDatabase.query(TABLE_HISTORY, new String[]{KEY_ID, KEY_URL, KEY_TITLE},
KEY_URL + " = ?", new String[]{url}, null, null, null, null);
String m = null;
if (cursor != null) {
cursor.moveToFirst();
m = cursor.getString(0);
cursor.close();
}
return m;
}
@NonNull
public synchronized List<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";
Cursor cursor = mDatabase.rawQuery(selectQuery, null);
int n = 0;
if (cursor.moveToFirst()) {
do {
HistoryItem item = new HistoryItem();
item.setUrl(cursor.getString(1));
item.setTitle(cursor.getString(2));
item.setImageId(R.drawable.ic_history);
itemList.add(item);
n++;
} while (cursor.moveToNext() && n < 5);
}
cursor.close();
return itemList;
}
@NonNull
public synchronized List<HistoryItem> getLastHundredItems() {
mDatabase = openIfNecessary();
List<HistoryItem> itemList = new ArrayList<>(100);
String selectQuery = "SELECT * FROM " + TABLE_HISTORY + " ORDER BY " + KEY_TIME_VISITED
+ " DESC";
Cursor cursor = mDatabase.rawQuery(selectQuery, null);
int counter = 0;
if (cursor.moveToFirst()) {
do {
HistoryItem item = new HistoryItem();
item.setUrl(cursor.getString(1));
item.setTitle(cursor.getString(2));
item.setImageId(R.drawable.ic_history);
itemList.add(item);
counter++;
} while (cursor.moveToNext() && counter < 100);
}
cursor.close();
return itemList;
}
@NonNull
public synchronized List<HistoryItem> getAllHistoryItems() {
mDatabase = openIfNecessary();
List<HistoryItem> itemList = new ArrayList<>();
String selectQuery = "SELECT * FROM " + TABLE_HISTORY + " ORDER BY " + KEY_TIME_VISITED
+ " DESC";
Cursor cursor = mDatabase.rawQuery(selectQuery, null);
if (cursor.moveToFirst()) {
do {
HistoryItem item = new HistoryItem();
item.setUrl(cursor.getString(1));
item.setTitle(cursor.getString(2));
item.setImageId(R.drawable.ic_history);
itemList.add(item);
} while (cursor.moveToNext());
}
cursor.close();
return itemList;
}
public synchronized int getHistoryItemsCount() {
mDatabase = openIfNecessary();
String countQuery = "SELECT * FROM " + TABLE_HISTORY;
Cursor cursor = mDatabase.rawQuery(countQuery, null);
int n = cursor.getCount();
cursor.close();
return n;
}
}
@@ -0,0 +1,158 @@
/*
* Copyright 2014 A.C.R. Development
*/
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
@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;
public HistoryItem() {}
public HistoryItem(@NonNull HistoryItem item) {
this.mUrl = item.mUrl;
this.mTitle = item.mTitle;
this.mFolder = item.mFolder;
this.mOrder = item.mOrder;
this.mIsFolder = item.mIsFolder;
}
public HistoryItem(@NonNull String url, @NonNull String title) {
Preconditions.checkNonNull(url);
Preconditions.checkNonNull(title);
this.mUrl = url;
this.mTitle = title;
this.mBitmap = null;
}
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;
}
public int getImageId() {
return this.mImageId;
}
public void setImageId(int id) {
this.mImageId = id;
}
public void setBitmap(Bitmap image) {
mBitmap = image;
}
public void setFolder(@Nullable String folder) {
mFolder = (folder == null) ? "" : folder;
}
public void setOrder(int order) {
mOrder = order;
}
public int getOrder() {
return mOrder;
}
@NonNull
public String getFolder() {
return mFolder;
}
@Nullable
public Bitmap getBitmap() {
return mBitmap;
}
@NonNull
public String getUrl() {
return this.mUrl;
}
public void setUrl(@Nullable String url) {
this.mUrl = (url == null) ? "" : url;
}
@NonNull
public String getTitle() {
return this.mTitle;
}
public void setTitle(@Nullable String title) {
this.mTitle = (title == null) ? "" : title;
}
public void setIsFolder(boolean isFolder) {
mIsFolder = isFolder;
}
public boolean isFolder() {
return mIsFolder;
}
@NonNull
@Override
public String toString() {
return mTitle;
}
@Override
public int compareTo(@NonNull HistoryItem another) {
int compare = this.mTitle.compareTo(another.mTitle);
if (compare == 0) {
return this.mUrl.compareTo(another.mUrl);
}
return compare;
}
@Override
public boolean equals(@Nullable Object object) {
if (this == object) return true;
if (object == null) return false;
if (!(object instanceof HistoryItem)) return false;
HistoryItem that = (HistoryItem) object;
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 = mUrl.hashCode();
result = 31 * result + mImageId;
result = 31 * result + mTitle.hashCode();
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();
}
}
@@ -0,0 +1,347 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.download;
import android.app.DownloadManager;
import android.content.ActivityNotFoundException;
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;
/**
* Handle download requests
*/
public class DownloadHandler {
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 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(@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
|| !contentDisposition.regionMatches(true, 0, "attachment", 0, 10)) {
// query the package manager to see if there's a registered handler
// that matches.
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(url), mimetype);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
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) {
// If we resolved to ourselves, we don't want to attempt to
// load the url only to try and download it again.
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 {
context.startActivity(intent);
return;
} catch (ActivityNotFoundException ex) {
// Best behavior is to fall back to a download in this
// case
}
}
}
}
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
@NonNull
private static String encodePath(@NonNull String path) {
char[] chars = path.toCharArray();
boolean needed = false;
for (char c : chars) {
if (c == '[' || c == ']' || c == '|') {
needed = true;
break;
}
}
if (!needed) {
return path;
}
StringBuilder sb = new StringBuilder("");
for (char c : chars) {
if (c == '[' || c == ']' || c == '|') {
sb.append('%');
sb.append(Integer.toHexString(c));
} else {
sb.append(c);
}
}
return sb.toString();
}
/**
* Notify the host application a download should be done, even if there is a
* streaming viewer available for thise type.
*
* @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(@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();
if (!status.equals(Environment.MEDIA_MOUNTED)) {
int title;
String msg;
// Check to see if the SDCard is busy, same as the music app
if (status.equals(Environment.MEDIA_SHARED)) {
msg = context.getString(R.string.download_sdcard_busy_dlg_msg);
title = R.string.download_sdcard_busy_dlg_title;
} else {
msg = context.getString(R.string.download_no_sdcard_dlg_msg);
title = R.string.download_no_sdcard_dlg_title;
}
new AlertDialog.Builder(context).setTitle(title)
.setIcon(android.R.drawable.ic_dialog_alert).setMessage(msg)
.setPositiveButton(R.string.action_ok, null).show();
return;
}
// java.net.URI is a lot stricter than KURL so we have to encode some
// extra characters. Fix for b 2538060 and b 1634719
WebAddress webAddress;
try {
webAddress = new WebAddress(url);
webAddress.setPath(encodePath(webAddress.getPath()));
} catch (Exception e) {
// This only happens for very bad urls, we want to catch the
// exception here
Log.e(TAG, "Exception while trying to parse url '" + url + '\'', e);
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.problem_download));
return;
}
String addressString = webAddress.toString();
Uri uri = Uri.parse(addressString);
final DownloadManager.Request request;
try {
request = new DownloadManager.Request(uri);
} catch (IllegalArgumentException e) {
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.cannot_download));
return;
}
request.setMimeType(mimetype);
// set downloaded file destination to /sdcard/Download.
// or, should it be set to one of several Environment.DIRECTORY* dirs
// depending on mimetype?
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_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(context, request, addressString, cookies, userAgent).start();
} else {
Log.d(TAG, "Valid mimetype, attempting to download");
final DownloadManager manager = (DownloadManager) context
.getSystemService(Context.DOWNLOAD_SERVICE);
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;
}
}
@@ -0,0 +1,122 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.download;
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.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
* can analyse the mimetype and make any correction needed before we give the
* URL to the download manager. This operation is needed when the user
* long-clicks on a link or image and we don't know the mimetype. If the user
* 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.
*/
class FetchUrlMimeType extends Thread {
private final Context mContext;
private final DownloadManager.Request mRequest;
private final String mUri;
private final String mCookies;
private final String mUserAgent;
public FetchUrlMimeType(Context context, DownloadManager.Request request, String uri,
String cookies, String userAgent) {
mContext = context;
mRequest = request;
mUri = uri;
mCookies = cookies;
mUserAgent = userAgent;
}
@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;
try {
URL url = new URL(mUri);
connection = (HttpURLConnection) url.openConnection();
if (mCookies != null && !mCookies.isEmpty()) {
connection.addRequestProperty("Cookie", mCookies);
connection.setRequestProperty("User-Agent", mUserAgent);
}
connection.connect();
// We could get a redirect here, but if we do lets let
// the download manager take care of it, and thus trust that
// the server sends the right mimetype
if (connection.getResponseCode() == 200) {
String header = connection.getHeaderField("Content-Type");
if (header != null) {
mimeType = header;
final int semicolonIndex = mimeType.indexOf(';');
if (semicolonIndex != -1) {
mimeType = mimeType.substring(0, semicolonIndex);
}
}
String contentDispositionHeader = connection.getHeaderField("Content-Disposition");
if (contentDispositionHeader != null) {
contentDisposition = contentDispositionHeader;
}
}
} catch (@NonNull IllegalArgumentException | IOException ex) {
if (connection != null)
connection.disconnect();
} finally {
if (connection != null)
connection.disconnect();
}
String filename = "";
if (mimeType != null) {
if (mimeType.equalsIgnoreCase("text/plain")
|| mimeType.equalsIgnoreCase("application/octet-stream")) {
String newMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
MimeTypeMap.getFileExtensionFromUrl(mUri));
if (newMimeType != null) {
mRequest.setMimeType(newMimeType);
}
}
filename = URLUtil.guessFileName(mUri, contentDisposition, mimeType);
mRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename);
}
// Start the download
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));
}
});
}
}
@@ -0,0 +1,75 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.download;
import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.webkit.DownloadListener;
import android.webkit.URLUtil;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.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;
@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) {
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;
}
}
};
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
}
});
}
}
@@ -0,0 +1,172 @@
/*
* Copyright 2014 A.C.R. Development
*/
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;
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
*/
class WebAddress {
private String mScheme;
private String mHost;
private int mPort;
private String mPath;
private String mAuthInfo;
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 */"(/?[^#]*)?" +
/* anchor */".*", Pattern.CASE_INSENSITIVE);
/**
* Parses given URI-like string.
*/
public WebAddress(@Nullable String address) throws IllegalArgumentException {
if (address == null) {
throw new IllegalArgumentException("address can't be null");
}
mScheme = "";
mHost = "";
mPort = -1;
mPath = "/";
mAuthInfo = "";
Matcher m = sAddressPattern.matcher(address);
String t;
if (!m.matches()) {
throw new IllegalArgumentException("Parsing of address '" + address + "' failed");
}
t = m.group(MATCH_GROUP_SCHEME);
if (t != null) {
mScheme = t.toLowerCase(Locale.ROOT);
}
t = m.group(MATCH_GROUP_AUTHORITY);
if (t != null) {
mAuthInfo = t;
}
t = m.group(MATCH_GROUP_HOST);
if (t != null) {
mHost = t;
}
t = m.group(MATCH_GROUP_PORT);
if (t != null && !t.isEmpty()) {
// The ':' character is not returned by the regex.
try {
mPort = Integer.parseInt(t);
} catch (NumberFormatException ex) {
throw new RuntimeException("Parsing of port number failed", ex);
}
}
t = m.group(MATCH_GROUP_PATH);
if (t != null && !t.isEmpty()) {
/*
* handle busted myspace frontpage redirect with missing initial "/"
*/
if (t.charAt(0) == '/') {
mPath = t;
} else {
mPath = '/' + t;
}
}
/*
* Get port from scheme or scheme from port, if necessary and possible
*/
if (mPort == 443 && mScheme != null && mScheme.isEmpty()) {
mScheme = "https";
} else if (mPort == -1) {
if ("https".equals(mScheme)) {
mPort = 443;
} else {
mPort = 80; // default
}
}
if (mScheme != null && mScheme.isEmpty()) {
mScheme = "http";
}
}
@NonNull
@Override
public String toString() {
String port = "";
if ((mPort != 443 && "https".equals(mScheme)) || (mPort != 80 && "http".equals(mScheme))) {
port = ':' + Integer.toString(mPort);
}
String authInfo = "";
if (!mAuthInfo.isEmpty()) {
authInfo = mAuthInfo + '@';
}
return mScheme + "://" + authInfo + mHost + port + mPath;
}
public void setScheme(String scheme) {
mScheme = scheme;
}
public String getScheme() {
return mScheme;
}
public void setHost(@NonNull String host) {
mHost = host;
}
public String getHost() {
return mHost;
}
public void setPort(int port) {
mPort = port;
}
public int getPort() {
return mPort;
}
public void setPath(String path) {
mPath = path;
}
public String getPath() {
return mPath;
}
public void setAuthInfo(String authInfo) {
mAuthInfo = authInfo;
}
public String getAuthInfo() {
return mAuthInfo;
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.fragment;
import android.app.Activity;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import acr.browser.lightning.R;
public class AboutSettingsFragment extends PreferenceFragment {
private Activity mActivity;
private static final String SETTINGS_VERSION = "pref_version";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preference_about);
mActivity = getActivity();
Preference version = findPreference(SETTINGS_VERSION);
version.setSummary(getVersion());
}
private String getVersion() {
try {
PackageInfo p = mActivity.getPackageManager().getPackageInfo(mActivity.getPackageName(), 0);
return p.versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return "1.0";
}
}
}
@@ -0,0 +1,209 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.fragment;
import android.app.Activity;
import android.content.DialogInterface;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import java.util.Arrays;
import java.util.List;
import acr.browser.lightning.R;
import acr.browser.lightning.constant.Constants;
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";
private static final String SETTINGS_COOKIESINKOGNITO = "incognito_cookies";
private static final String SETTINGS_RESTORETABS = "restore_tabs";
private static final String SETTINGS_RENDERINGMODE = "rendering_mode";
private static final String SETTINGS_URLCONTENT = "url_contents";
private static final String SETTINGS_TEXTENCODING = "text_encoding";
private Activity mActivity;
private CheckBoxPreference cbAllowPopups, cbenablecookies, cbcookiesInkognito, cbrestoreTabs;
private Preference renderingmode, urlcontent, textEncoding;
private CharSequence[] mUrlOptions;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preference_advanced);
mActivity = getActivity();
initPrefs();
}
private void initPrefs() {
renderingmode = findPreference(SETTINGS_RENDERINGMODE);
textEncoding = findPreference(SETTINGS_TEXTENCODING);
urlcontent = findPreference(SETTINGS_URLCONTENT);
cbAllowPopups = (CheckBoxPreference) findPreference(SETTINGS_NEWWINDOW);
cbenablecookies = (CheckBoxPreference) findPreference(SETTINGS_ENABLECOOKIES);
cbcookiesInkognito = (CheckBoxPreference) findPreference(SETTINGS_COOKIESINKOGNITO);
cbrestoreTabs = (CheckBoxPreference) findPreference(SETTINGS_RESTORETABS);
renderingmode.setOnPreferenceClickListener(this);
textEncoding.setOnPreferenceClickListener(this);
urlcontent.setOnPreferenceClickListener(this);
cbAllowPopups.setOnPreferenceChangeListener(this);
cbenablecookies.setOnPreferenceChangeListener(this);
cbcookiesInkognito.setOnPreferenceChangeListener(this);
cbrestoreTabs.setOnPreferenceChangeListener(this);
switch (mPreferenceManager.getRenderingMode()) {
case 0:
renderingmode.setSummary(getString(R.string.name_normal));
break;
case 1:
renderingmode.setSummary(getString(R.string.name_inverted));
break;
case 2:
renderingmode.setSummary(getString(R.string.name_grayscale));
break;
case 3:
renderingmode.setSummary(getString(R.string.name_inverted_grayscale));
break;
case 4:
renderingmode.setSummary(getString(R.string.name_increase_contrast));
break;
}
textEncoding.setSummary(mPreferenceManager.getTextEncoding());
mUrlOptions = getResources().getStringArray(R.array.url_content_array);
int option = mPreferenceManager.getUrlBoxContentChoice();
urlcontent.setSummary(mUrlOptions[option]);
cbAllowPopups.setChecked(mPreferenceManager.getPopupsEnabled());
cbenablecookies.setChecked(mPreferenceManager.getCookiesEnabled());
cbcookiesInkognito.setChecked(mPreferenceManager.getIncognitoCookiesEnabled());
cbrestoreTabs.setChecked(mPreferenceManager.getRestoreLostTabsEnabled());
}
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_RENDERINGMODE:
renderPicker();
return true;
case SETTINGS_URLCONTENT:
urlBoxPicker();
return true;
case SETTINGS_TEXTENCODING:
textEncodingPicker();
return true;
default:
return false;
}
}
@Override
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
// switch preferences
switch (preference.getKey()) {
case SETTINGS_NEWWINDOW:
mPreferenceManager.setPopupsEnabled((Boolean) newValue);
cbAllowPopups.setChecked((Boolean) newValue);
return true;
case SETTINGS_ENABLECOOKIES:
mPreferenceManager.setCookiesEnabled((Boolean) newValue);
cbenablecookies.setChecked((Boolean) newValue);
return true;
case SETTINGS_COOKIESINKOGNITO:
mPreferenceManager.setIncognitoCookiesEnabled((Boolean) newValue);
cbcookiesInkognito.setChecked((Boolean) newValue);
return true;
case SETTINGS_RESTORETABS:
mPreferenceManager.setRestoreLostTabsEnabled((Boolean) newValue);
cbrestoreTabs.setChecked((Boolean) newValue);
return true;
default:
return false;
}
}
private void renderPicker() {
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.rendering_mode));
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_increase_contrast)};
int n = mPreferenceManager.getRenderingMode();
picker.setSingleChoiceItems(chars, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferenceManager.setRenderingMode(which);
switch (which) {
case 0:
renderingmode.setSummary(getString(R.string.name_normal));
break;
case 1:
renderingmode.setSummary(getString(R.string.name_inverted));
break;
case 2:
renderingmode.setSummary(getString(R.string.name_grayscale));
break;
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), null);
picker.show();
}
private void textEncodingPicker() {
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(mPreferenceManager.getTextEncoding());
picker.setSingleChoiceItems(Constants.TEXT_ENCODINGS, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferenceManager.setTextEncoding(Constants.TEXT_ENCODINGS[which]);
textEncoding.setSummary(Constants.TEXT_ENCODINGS[which]);
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
picker.show();
}
private void urlBoxPicker() {
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.url_contents));
int n = mPreferenceManager.getUrlBoxContentChoice();
picker.setSingleChoiceItems(mUrlOptions, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferenceManager.setUrlBoxContentChoice(which);
if (which < mUrlOptions.length) {
urlcontent.setSummary(mUrlOptions[which]);
}
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
picker.show();
}
}
@@ -0,0 +1,406 @@
/*
* Copyright 2014 A.C.R. Development
*/
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.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";
@Nullable private Activity mActivity;
@Inject BookmarkManager mBookmarkManager;
private File[] mFileList;
private String[] mFileNameList;
@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();
mSync = new BookmarkLocalSync(mActivity);
initPrefs();
PermissionsManager permissionsManager = PermissionsManager.getInstance();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
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 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);
}
});
}
});
}
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_EXPORT:
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(getActivity(), REQUIRED_PERMISSIONS,
new PermissionsResultAction() {
@Override
public void onGranted() {
mBookmarkManager.exportBookmarks(getActivity());
}
@Override
public void onDenied(String permission) {
//TODO Show message
}
});
return true;
case SETTINGS_IMPORT:
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 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;
} else {
file = mPath;
}
try {
file.mkdirs();
} catch (SecurityException e) {
e.printStackTrace();
}
if (file.exists()) {
mFileList = file.listFiles();
} else {
mFileList = new File[0];
}
if (mFileList == null) {
mFileNameList = new String[0];
mFileList = new File[0];
} else {
Arrays.sort(mFileList, new SortName());
mFileNameList = new String[mFileList.length];
}
for (int n = 0; n < mFileList.length; n++) {
mFileNameList[n] = mFileList[n].getName();
}
}
private static class SortName implements Comparator<File> {
@Override
public int compare(@NonNull File a, @NonNull File b) {
if (a.isDirectory() && b.isDirectory())
return a.getName().compareTo(b.getName());
if (a.isDirectory())
return -1;
if (b.isDirectory())
return 1;
if (a.isFile() && b.isFile())
return a.getName().compareTo(b.getName());
else
return 1;
}
}
private void createDialog() {
final AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
final String title = getString(R.string.title_chooser);
builder.setTitle(title + ": " + Environment.getExternalStorageDirectory());
if (mFileList == null) {
builder.show();
}
builder.setItems(mFileNameList, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (mFileList[which].isDirectory()) {
builder.setTitle(title + ": " + mFileList[which]);
loadFileList(mFileList[which]);
builder.setItems(mFileNameList, this);
builder.show();
} else {
mBookmarkManager.importBookmarksFromFile(mFileList[which], getActivity());
}
}
});
builder.show();
}
}
@@ -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;
}
}
@@ -0,0 +1,231 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.fragment;
import android.app.Activity;
import android.content.DialogInterface;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import acr.browser.lightning.R;
public class DisplaySettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
private static final String SETTINGS_HIDESTATUSBAR = "fullScreenOption";
private static final String SETTINGS_FULLSCREEN = "fullscreen";
private static final String SETTINGS_VIEWPORT = "wideViewPort";
private static final String SETTINGS_OVERVIEWMODE = "overViewMode";
private static final String SETTINGS_REFLOW = "text_reflow";
private static final String SETTINGS_THEME = "app_theme";
private static final String SETTINGS_TEXTSIZE = "text_size";
private static final float XXLARGE = 30.0f;
private static final float XLARGE = 26.0f;
private static final float LARGE = 22.0f;
private static final float MEDIUM = 18.0f;
private static final float SMALL = 14.0f;
private static final float XSMALL = 10.0f;
private Activity mActivity;
private CheckBoxPreference cbstatus, cbfullscreen, cbviewport, cboverview, cbreflow;
private Preference theme;
private String[] mThemeOptions;
private int mCurrentTheme;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preference_display);
mActivity = getActivity();
initPrefs();
}
private void initPrefs() {
// mPreferences storage
mThemeOptions = this.getResources().getStringArray(R.array.themes);
mCurrentTheme = mPreferenceManager.getUseTheme();
theme = findPreference(SETTINGS_THEME);
Preference textsize = findPreference(SETTINGS_TEXTSIZE);
cbstatus = (CheckBoxPreference) findPreference(SETTINGS_HIDESTATUSBAR);
cbfullscreen = (CheckBoxPreference) findPreference(SETTINGS_FULLSCREEN);
cbviewport = (CheckBoxPreference) findPreference(SETTINGS_VIEWPORT);
cboverview = (CheckBoxPreference) findPreference(SETTINGS_OVERVIEWMODE);
cbreflow = (CheckBoxPreference) findPreference(SETTINGS_REFLOW);
theme.setOnPreferenceClickListener(this);
textsize.setOnPreferenceClickListener(this);
cbstatus.setOnPreferenceChangeListener(this);
cbfullscreen.setOnPreferenceChangeListener(this);
cbviewport.setOnPreferenceChangeListener(this);
cboverview.setOnPreferenceChangeListener(this);
cbreflow.setOnPreferenceChangeListener(this);
cbstatus.setChecked(mPreferenceManager.getHideStatusBarEnabled());
cbfullscreen.setChecked(mPreferenceManager.getFullScreenEnabled());
cbviewport.setChecked(mPreferenceManager.getUseWideViewportEnabled());
cboverview.setChecked(mPreferenceManager.getOverviewModeEnabled());
cbreflow.setChecked(mPreferenceManager.getTextReflowEnabled());
theme.setSummary(mThemeOptions[mPreferenceManager.getUseTheme()]);
}
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_THEME:
themePicker();
return true;
case SETTINGS_TEXTSIZE:
textSizePicker();
return true;
default:
return false;
}
}
@Override
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
// switch preferences
switch (preference.getKey()) {
case SETTINGS_HIDESTATUSBAR:
mPreferenceManager.setHideStatusBarEnabled((Boolean) newValue);
cbstatus.setChecked((Boolean) newValue);
return true;
case SETTINGS_FULLSCREEN:
mPreferenceManager.setFullScreenEnabled((Boolean) newValue);
cbfullscreen.setChecked((Boolean) newValue);
return true;
case SETTINGS_VIEWPORT:
mPreferenceManager.setUseWideViewportEnabled((Boolean) newValue);
cbviewport.setChecked((Boolean) newValue);
return true;
case SETTINGS_OVERVIEWMODE:
mPreferenceManager.setOverviewModeEnabled((Boolean) newValue);
cboverview.setChecked((Boolean) newValue);
return true;
case SETTINGS_REFLOW:
mPreferenceManager.setTextReflowEnabled((Boolean) newValue);
cbreflow.setChecked((Boolean) newValue);
return true;
default:
return false;
}
}
private void textSizePicker() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
LayoutInflater inflater = getActivity().getLayoutInflater();
LinearLayout view = (LinearLayout) inflater.inflate(R.layout.seek_layout, null);
final SeekBar bar = (SeekBar) view.findViewById(R.id.text_size_seekbar);
final TextView sample = new TextView(getActivity());
sample.setText(R.string.untitled);
sample.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT));
sample.setGravity(Gravity.CENTER_HORIZONTAL);
view.addView(sample);
bar.setOnSeekBarChangeListener(new TextSeekBarListener(sample));
final int MAX = 5;
bar.setMax(MAX);
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) {
mPreferenceManager.setTextSize(MAX - bar.getProgress());
}
});
builder.show();
}
private static float getTextSize(int size) {
switch (size) {
case 0:
return XSMALL;
case 1:
return SMALL;
case 2:
return MEDIUM;
case 3:
return LARGE;
case 4:
return XLARGE;
case 5:
return XXLARGE;
default:
return MEDIUM;
}
}
private void themePicker() {
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.theme));
int n = mPreferenceManager.getUseTheme();
picker.setSingleChoiceItems(mThemeOptions, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferenceManager.setUseTheme(which);
if (which < mThemeOptions.length) {
theme.setSummary(mThemeOptions[which]);
}
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (mCurrentTheme != mPreferenceManager.getUseTheme()) {
getActivity().onBackPressed();
}
}
});
picker.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
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) {
}
}
}
@@ -0,0 +1,598 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.fragment;
import android.app.Activity;
import android.content.DialogInterface;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
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.text.TextWatcher;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
import acr.browser.lightning.R;
import acr.browser.lightning.constant.Constants;
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 LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
private static final String SETTINGS_PROXY = "proxy";
private static final String SETTINGS_FLASH = "cb_flash";
private static final String SETTINGS_ADS = "cb_ads";
private static final String SETTINGS_IMAGES = "cb_images";
private static final String SETTINGS_JAVASCRIPT = "cb_javascript";
private static final String SETTINGS_COLORMODE = "cb_colormode";
private static final String SETTINGS_USERAGENT = "agent";
private static final String SETTINGS_DOWNLOAD = "download";
private static final String SETTINGS_HOME = "home";
private static final String SETTINGS_SEARCHENGINE = "search";
private static final String SETTINGS_GOOGLESUGGESTIONS = "google_suggestions";
private static final String SETTINGS_DRAWERTABS = "cb_drawertabs";
private Activity mActivity;
private static final int API = android.os.Build.VERSION.SDK_INT;
private CharSequence[] mProxyChoices;
private Preference proxy, useragent, downloadloc, home, searchengine;
private String mDownloadLocation;
private int mAgentChoice;
private String mHomepage;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preference_general);
mActivity = getActivity();
initPrefs();
}
private void initPrefs() {
proxy = findPreference(SETTINGS_PROXY);
useragent = findPreference(SETTINGS_USERAGENT);
downloadloc = findPreference(SETTINGS_DOWNLOAD);
home = findPreference(SETTINGS_HOME);
searchengine = findPreference(SETTINGS_SEARCHENGINE);
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);
downloadloc.setOnPreferenceClickListener(this);
home.setOnPreferenceClickListener(this);
searchengine.setOnPreferenceClickListener(this);
cbFlash.setOnPreferenceChangeListener(this);
cbAds.setOnPreferenceChangeListener(this);
cbImages.setOnPreferenceChangeListener(this);
cbJsScript.setOnPreferenceChangeListener(this);
cbColorMode.setOnPreferenceChangeListener(this);
cbgooglesuggest.setOnPreferenceChangeListener(this);
cbDrawerTabs.setOnPreferenceChangeListener(this);
mAgentChoice = mPreferenceManager.getUserAgentChoice();
mHomepage = mPreferenceManager.getHomepage();
mDownloadLocation = mPreferenceManager.getDownloadDirectory();
mProxyChoices = getResources().getStringArray(R.array.proxy_choices_array);
int choice = mPreferenceManager.getProxyChoice();
if (choice == Constants.PROXY_MANUAL) {
proxy.setSummary(mPreferenceManager.getProxyHost() + ':' + mPreferenceManager.getProxyPort());
} else {
proxy.setSummary(mProxyChoices[choice]);
}
if (API >= Build.VERSION_CODES.KITKAT) {
mPreferenceManager.setFlashSupport(0);
}
setSearchEngineSummary(mPreferenceManager.getSearchChoice());
downloadloc.setSummary(mDownloadLocation);
if (mHomepage.contains("about:home")) {
home.setSummary(getResources().getString(R.string.action_homepage));
} else if (mHomepage.contains("about:blank")) {
home.setSummary(getResources().getString(R.string.action_blank));
} else if (mHomepage.contains("about:bookmarks")) {
home.setSummary(getResources().getString(R.string.action_bookmarks));
} else {
home.setSummary(mHomepage);
}
switch (mAgentChoice) {
case 1:
useragent.setSummary(getResources().getString(R.string.agent_default));
break;
case 2:
useragent.setSummary(getResources().getString(R.string.agent_desktop));
break;
case 3:
useragent.setSummary(getResources().getString(R.string.agent_mobile));
break;
case 4:
useragent.setSummary(getResources().getString(R.string.agent_custom));
}
int flashNum = mPreferenceManager.getFlashSupport();
boolean imagesBool = mPreferenceManager.getBlockImagesEnabled();
boolean enableJSBool = mPreferenceManager.getJavaScriptEnabled();
cbAds.setEnabled(Constants.FULL_VERSION);
cbFlash.setEnabled(API < Build.VERSION_CODES.KITKAT);
cbImages.setChecked(imagesBool);
cbJsScript.setChecked(enableJSBool);
cbFlash.setChecked(flashNum > 0);
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 = mPreferenceManager.getSearchUrl();
getSearchUrl.setText(mSearchUrl);
urlPicker.setView(getSearchUrl);
urlPicker.setPositiveButton(getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String text = getSearchUrl.getText().toString();
mPreferenceManager.setSearchUrl(text);
searchengine.setSummary(getResources().getString(R.string.custom_url) + ": "
+ text);
}
});
urlPicker.show();
}
private void getFlashChoice() {
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle(mActivity.getResources().getString(R.string.title_flash));
builder.setMessage(getResources().getString(R.string.flash))
.setCancelable(true)
.setPositiveButton(getResources().getString(R.string.action_manual),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
mPreferenceManager.setFlashSupport(1);
}
})
.setNegativeButton(getResources().getString(R.string.action_auto),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferenceManager.setFlashSupport(2);
}
}).setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
mPreferenceManager.setFlashSupport(0);
}
});
AlertDialog alert = builder.create();
alert.show();
}
private void proxyChoicePicker() {
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.http_proxy));
picker.setSingleChoiceItems(mProxyChoices, mPreferenceManager.getProxyChoice(),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
setProxyChoice(which);
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
picker.show();
}
private void setProxyChoice(int choice) {
switch (choice) {
case Constants.PROXY_ORBOT:
choice = ProxyUtils.setProxyChoice(choice, mActivity);
break;
case Constants.PROXY_I2P:
choice = ProxyUtils.setProxyChoice(choice, mActivity);
break;
case Constants.PROXY_MANUAL:
manualProxyPicker();
break;
}
mPreferenceManager.setProxyChoice(choice);
if (choice < mProxyChoices.length)
proxy.setSummary(mProxyChoices[choice]);
}
private void manualProxyPicker() {
View v = mActivity.getLayoutInflater().inflate(R.layout.picker_manual_proxy, null);
final EditText eProxyHost = (EditText) v.findViewById(R.id.proxyHost);
final EditText eProxyPort = (EditText) v.findViewById(R.id.proxyPort);
// Limit the number of characters since the port needs to be of type int
// Use input filters to limite the EditText length and determine the max
// length by using length of integer MAX_VALUE
int maxCharacters = Integer.toString(Integer.MAX_VALUE).length();
InputFilter[] filterArray = new InputFilter[1];
filterArray[0] = new InputFilter.LengthFilter(maxCharacters - 1);
eProxyPort.setFilters(filterArray);
eProxyHost.setText(mPreferenceManager.getProxyHost());
eProxyPort.setText(Integer.toString(mPreferenceManager.getProxyPort()));
new AlertDialog.Builder(mActivity)
.setTitle(R.string.manual_proxy)
.setView(v)
.setPositiveButton(R.string.action_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
String proxyHost = eProxyHost.getText().toString();
int proxyPort;
try {
// Try/Catch in case the user types an empty string or a number
// larger than max integer
proxyPort = Integer.parseInt(eProxyPort.getText().toString());
} catch (NumberFormatException ignored) {
proxyPort = mPreferenceManager.getProxyPort();
}
mPreferenceManager.setProxyHost(proxyHost);
mPreferenceManager.setProxyPort(proxyPort);
proxy.setSummary(proxyHost + ':' + proxyPort);
}
}).show();
}
private void searchDialog() {
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.title_search_engine));
CharSequence[] chars = {getResources().getString(R.string.custom_url), "Google",
"Ask", "Bing", "Yahoo", "StartPage", "StartPage (Mobile)",
"DuckDuckGo (Privacy)", "DuckDuckGo Lite (Privacy)", "Baidu (Chinese)",
"Yandex (Russian)"};
int n = mPreferenceManager.getSearchChoice();
picker.setSingleChoiceItems(chars, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferenceManager.setSearchChoice(which);
setSearchEngineSummary(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 = mPreferenceManager.getHomepage();
int n;
if (mHomepage.contains("about:home")) {
n = 1;
} else if (mHomepage.contains("about:blank")) {
n = 2;
} else if (mHomepage.contains("about:bookmarks")) {
n = 3;
} else {
n = 4;
}
picker.setSingleChoiceItems(R.array.homepage, n - 1,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which + 1) {
case 1:
mPreferenceManager.setHomepage("about:home");
home.setSummary(getResources().getString(R.string.action_homepage));
break;
case 2:
mPreferenceManager.setHomepage("about:blank");
home.setSummary(getResources().getString(R.string.action_blank));
break;
case 3:
mPreferenceManager.setHomepage("about:bookmarks");
home.setSummary(getResources().getString(R.string.action_bookmarks));
break;
case 4:
homePicker();
break;
}
}
});
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
picker.show();
}
private void homePicker() {
final AlertDialog.Builder homePicker = new AlertDialog.Builder(mActivity);
homePicker.setTitle(getResources().getString(R.string.title_custom_homepage));
final EditText getHome = new EditText(mActivity);
mHomepage = mPreferenceManager.getHomepage();
if (!mHomepage.startsWith("about:")) {
getHome.setText(mHomepage);
} else {
String defaultUrl = "https://www.google.com";
getHome.setText(defaultUrl);
}
homePicker.setView(getHome);
homePicker.setPositiveButton(getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String text = getHome.getText().toString();
mPreferenceManager.setHomepage(text);
home.setSummary(text);
}
});
homePicker.show();
}
private void downloadLocDialog() {
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.title_download_location));
mDownloadLocation = mPreferenceManager.getDownloadDirectory();
int n;
if (mDownloadLocation.contains(Environment.DIRECTORY_DOWNLOADS)) {
n = 0;
} else {
n = 1;
}
picker.setSingleChoiceItems(R.array.download_folder, n,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0:
mPreferenceManager.setDownloadDirectory(DownloadHandler.DEFAULT_DOWNLOAD_PATH);
downloadloc.setSummary(DownloadHandler.DEFAULT_DOWNLOAD_PATH);
break;
case 1:
downPicker();
break;
}
}
});
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 = mPreferenceManager.getUserAgentChoice();
agentPicker.setSingleChoiceItems(R.array.user_agent, mAgentChoice - 1,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferenceManager.setUserAgentChoice(which + 1);
switch (which + 1) {
case 1:
useragent.setSummary(getResources().getString(R.string.agent_default));
break;
case 2:
useragent.setSummary(getResources().getString(R.string.agent_desktop));
break;
case 3:
useragent.setSummary(getResources().getString(R.string.agent_mobile));
break;
case 4:
useragent.setSummary(getResources().getString(R.string.agent_custom));
agentPicker();
break;
}
}
});
agentPicker.setNeutralButton(getResources().getString(R.string.action_ok), null);
agentPicker.show();
}
private void agentPicker() {
final AlertDialog.Builder agentStringPicker = new AlertDialog.Builder(mActivity);
agentStringPicker.setTitle(getResources().getString(R.string.title_user_agent));
final EditText getAgent = new EditText(mActivity);
agentStringPicker.setView(getAgent);
agentStringPicker.setPositiveButton(getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String text = getAgent.getText().toString();
mPreferenceManager.setUserAgentString(text);
useragent.setSummary(getResources().getString(R.string.agent_custom));
}
});
agentStringPicker.show();
}
private void downPicker() {
final AlertDialog.Builder downLocationPicker = new AlertDialog.Builder(mActivity);
LinearLayout layout = new LinearLayout(mActivity);
downLocationPicker.setTitle(getResources().getString(R.string.title_download_location));
final EditText getDownload = new EditText(mActivity);
getDownload.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
getDownload.setText(mPreferenceManager.getDownloadDirectory());
final int errorColor = ContextCompat.getColor(getActivity(), R.color.error_red);
final int regularColor = ThemeUtils.getTextColor(getActivity());
getDownload.setTextColor(regularColor);
getDownload.addTextChangedListener(new DownloadLocationTextWatcher(getDownload, errorColor, regularColor));
getDownload.setText(mPreferenceManager.getDownloadDirectory());
layout.addView(getDownload);
downLocationPicker.setView(layout);
downLocationPicker.setPositiveButton(getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String text = getDownload.getText().toString();
text = DownloadHandler.addNecessarySlashes(text);
mPreferenceManager.setDownloadDirectory(text);
downloadloc.setSummary(text);
}
});
downLocationPicker.show();
}
private void setSearchEngineSummary(int which) {
switch (which) {
case 0:
searchUrlPicker();
break;
case 1:
searchengine.setSummary("Google");
break;
case 2:
searchengine.setSummary("Ask");
break;
case 3:
searchengine.setSummary("Bing");
break;
case 4:
searchengine.setSummary("Yahoo");
break;
case 5:
searchengine.setSummary("StartPage");
break;
case 6:
searchengine.setSummary("StartPage (Mobile)");
break;
case 7:
searchengine.setSummary("DuckDuckGo");
break;
case 8:
searchengine.setSummary("DuckDuckGo Lite");
break;
case 9:
searchengine.setSummary("Baidu");
break;
case 10:
searchengine.setSummary("Yandex");
}
}
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_PROXY:
proxyChoicePicker();
return true;
case SETTINGS_USERAGENT:
agentDialog();
return true;
case SETTINGS_DOWNLOAD:
downloadLocDialog();
return true;
case SETTINGS_HOME:
homepageDialog();
return true;
case SETTINGS_SEARCHENGINE:
searchDialog();
return true;
default:
return false;
}
}
@Override
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 (!Utils.isFlashInstalled(mActivity) && checked) {
Utils.createInformativeDialog(mActivity, R.string.title_warning, R.string.dialog_adobe_not_installed);
mPreferenceManager.setFlashSupport(0);
return false;
} else {
if (checked) {
getFlashChoice();
} else {
mPreferenceManager.setFlashSupport(0);
}
}
return true;
case SETTINGS_ADS:
mPreferenceManager.setAdBlockEnabled(checked);
return true;
case SETTINGS_IMAGES:
mPreferenceManager.setBlockImagesEnabled(checked);
return true;
case SETTINGS_JAVASCRIPT:
mPreferenceManager.setJavaScriptEnabled(checked);
return true;
case SETTINGS_COLORMODE:
mPreferenceManager.setColorModeEnabled(checked);
return true;
case SETTINGS_GOOGLESUGGESTIONS:
mPreferenceManager.setGoogleSearchSuggestionsEnabled(checked);
return true;
case SETTINGS_DRAWERTABS:
mPreferenceManager.setShowTabsInDrawer(checked);
return true;
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);
}
}
@@ -0,0 +1,248 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.fragment;
import android.app.Activity;
import android.content.DialogInterface;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.webkit.WebView;
import javax.inject.Inject;
import acr.browser.lightning.R;
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 LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
private static final String SETTINGS_LOCATION = "location";
private static final String SETTINGS_THIRDPCOOKIES = "third_party";
private static final String SETTINGS_SAVEPASSWORD = "password";
private static final String SETTINGS_CACHEEXIT = "clear_cache_exit";
private static final String SETTINGS_HISTORYEXIT = "clear_history_exit";
private static final String SETTINGS_COOKIEEXIT = "clear_cookies_exit";
private static final String SETTINGS_CLEARCACHE = "clear_cache";
private static final String SETTINGS_CLEARHISTORY = "clear_history";
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 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);
mActivity = getActivity();
initPrefs();
}
private void initPrefs() {
Preference clearcache = findPreference(SETTINGS_CLEARCACHE);
Preference clearhistory = findPreference(SETTINGS_CLEARHISTORY);
Preference clearcookies = findPreference(SETTINGS_CLEARCOOKIES);
Preference clearwebstorage = findPreference(SETTINGS_CLEARWEBSTORAGE);
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);
clearcookies.setOnPreferenceClickListener(this);
clearwebstorage.setOnPreferenceClickListener(this);
cblocation.setOnPreferenceChangeListener(this);
cb3cookies.setOnPreferenceChangeListener(this);
cbsavepasswords.setOnPreferenceChangeListener(this);
cbcacheexit.setOnPreferenceChangeListener(this);
cbhistoryexit.setOnPreferenceChangeListener(this);
cbcookiesexit.setOnPreferenceChangeListener(this);
cbwebstorageexit.setOnPreferenceChangeListener(this);
cbDoNotTrack.setOnPreferenceChangeListener(this);
cbIdentifyingHeaders.setOnPreferenceChangeListener(this);
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);
mMessageHandler = new MessageHandler(mActivity);
}
private static class MessageHandler extends Handler {
final Activity mHandlerContext;
public MessageHandler(Activity context) {
this.mHandlerContext = context;
}
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what) {
case 1:
Utils.showSnackbar(mHandlerContext, R.string.message_clear_history);
break;
case 2:
Utils.showSnackbar(mHandlerContext, R.string.message_cookies_cleared);
break;
}
super.handleMessage(msg);
}
}
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_CLEARCACHE:
clearCache();
return true;
case SETTINGS_CLEARHISTORY:
clearHistoryDialog();
return true;
case SETTINGS_CLEARCOOKIES:
clearCookiesDialog();
return true;
case SETTINGS_CLEARWEBSTORAGE:
clearWebStorage();
return true;
default:
return false;
}
}
private void clearHistoryDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle(getResources().getString(R.string.title_clear_history));
builder.setMessage(getResources().getString(R.string.dialog_history))
.setPositiveButton(getResources().getString(R.string.action_yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
BrowserApp.getIOThread().execute(new Runnable() {
@Override
public void run() {
clearHistory();
}
});
}
})
.setNegativeButton(getResources().getString(R.string.action_no), null).show();
}
private void clearCookiesDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle(getResources().getString(R.string.title_clear_cookies));
builder.setMessage(getResources().getString(R.string.dialog_cookies))
.setPositiveButton(getResources().getString(R.string.action_yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
BrowserApp.getTaskThread().execute(new Runnable() {
@Override
public void run() {
clearCookies();
}
});
}
})
.setNegativeButton(getResources().getString(R.string.action_no), null).show();
}
private void clearCache() {
WebView webView = new WebView(mActivity);
webView.clearCache(true);
webView.destroy();
Utils.showSnackbar(mActivity, R.string.message_cache_cleared);
}
private void clearHistory() {
WebUtils.clearHistory(getActivity(), mHistoryDatabase);
mMessageHandler.sendEmptyMessage(1);
}
private void clearCookies() {
WebUtils.clearCookies(getActivity());
mMessageHandler.sendEmptyMessage(2);
}
private void clearWebStorage() {
WebUtils.clearWebStorage();
Utils.showSnackbar(getActivity(), R.string.message_web_storage_cleared);
}
@Override
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
switch (preference.getKey()) {
case SETTINGS_LOCATION:
mPreferenceManager.setLocationEnabled((Boolean) newValue);
return true;
case SETTINGS_THIRDPCOOKIES:
mPreferenceManager.setBlockThirdPartyCookiesEnabled((Boolean) newValue);
return true;
case SETTINGS_SAVEPASSWORD:
mPreferenceManager.setSavePasswordsEnabled((Boolean) newValue);
return true;
case SETTINGS_CACHEEXIT:
mPreferenceManager.setClearCacheExit((Boolean) newValue);
return true;
case SETTINGS_HISTORYEXIT:
mPreferenceManager.setClearHistoryExitEnabled((Boolean) newValue);
return true;
case SETTINGS_COOKIEEXIT:
mPreferenceManager.setClearCookiesExitEnabled((Boolean) newValue);
return true;
case SETTINGS_WEBSTORAGEEXIT:
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) {}
}
}
@@ -0,0 +1,470 @@
package acr.browser.lightning.preference;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.download.DownloadHandler;
@Singleton
public class PreferenceManager {
private static class Name {
public static final String ADOBE_FLASH_SUPPORT = "enableflash";
public static final String BLOCK_ADS = "AdBlock";
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 = "downloadLocation";
public static final String FULL_SCREEN = "fullscreen";
public static final String HIDE_STATUS_BAR = "hidestatus";
public static final String HOMEPAGE = "home";
public static final String INCOGNITO_COOKIES = "incognitocookies";
public static final String JAVASCRIPT = "java";
public static final String LOCATION = "location";
public static final String OVERVIEW_MODE = "overviewmode";
public static final String POPUPS = "newwindows";
public static final String RESTORE_LOST_TABS = "restoreclosed";
public static final String SAVE_PASSWORDS = "passwords";
public static final String SEARCH = "search";
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 USE_WIDE_VIEWPORT = "wideviewport";
public static final String USER_AGENT = "agentchoose";
public static final String USER_AGENT_STRING = "userAgentString";
public static final String GOOGLE_SEARCH_SUGGESTIONS = "GoogleSearchSuggestions";
public static final String CLEAR_HISTORY_EXIT = "clearHistoryExit";
public static final String CLEAR_COOKIES_EXIT = "clearCookiesExit";
public static final String SAVE_URL = "saveUrl";
public static final String RENDERING_MODE = "renderMode";
public static final String BLOCK_THIRD_PARTY = "thirdParty";
public static final String ENABLE_COLOR_MODE = "colorMode";
public static final String URL_BOX_CONTENTS = "urlContent";
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 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";
public static final String USE_PROXY_HOST = "useProxyHost";
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";
}
@NonNull private final SharedPreferences mPrefs;
private static final String PREFERENCES = "settings";
@Inject
PreferenceManager(@NonNull final Context context) {
mPrefs = context.getSharedPreferences(PREFERENCES, 0);
}
public boolean getAdBlockEnabled() {
return mPrefs.getBoolean(Name.BLOCK_ADS, false);
}
public boolean getBlockImagesEnabled() {
return mPrefs.getBoolean(Name.BLOCK_IMAGES, false);
}
public boolean getBlockThirdPartyCookiesEnabled() {
return mPrefs.getBoolean(Name.BLOCK_THIRD_PARTY, false);
}
public boolean getCheckedForTor() {
return mPrefs.getBoolean(Name.INITIAL_CHECK_FOR_TOR, false);
}
public boolean getCheckedForI2P() {
return mPrefs.getBoolean(Name.INITIAL_CHECK_FOR_I2P, false);
}
public boolean getClearCacheExit() {
return mPrefs.getBoolean(Name.CLEAR_CACHE_EXIT, false);
}
public boolean getClearCookiesExitEnabled() {
return mPrefs.getBoolean(Name.CLEAR_COOKIES_EXIT, false);
}
public boolean getClearWebStorageExitEnabled() {
return mPrefs.getBoolean(Name.CLEAR_WEBSTORAGE_EXIT, false);
}
public boolean getClearHistoryExitEnabled() {
return mPrefs.getBoolean(Name.CLEAR_HISTORY_EXIT, false);
}
public boolean getColorModeEnabled() {
return mPrefs.getBoolean(Name.ENABLE_COLOR_MODE, true);
}
public boolean getCookiesEnabled() {
return mPrefs.getBoolean(Name.COOKIES, true);
}
@NonNull
public String getDownloadDirectory() {
return mPrefs.getString(Name.DOWNLOAD_DIRECTORY, DownloadHandler.DEFAULT_DOWNLOAD_PATH);
}
public int getFlashSupport() {
return mPrefs.getInt(Name.ADOBE_FLASH_SUPPORT, 0);
}
public boolean getFullScreenEnabled() {
return mPrefs.getBoolean(Name.FULL_SCREEN, false);
}
public boolean getGoogleSearchSuggestionsEnabled() {
return mPrefs.getBoolean(Name.GOOGLE_SEARCH_SUGGESTIONS, true);
}
public boolean getHideStatusBarEnabled() {
return mPrefs.getBoolean(Name.HIDE_STATUS_BAR, false);
}
@NonNull
public String getHomepage() {
return mPrefs.getString(Name.HOMEPAGE, Constants.HOMEPAGE);
}
public boolean getIncognitoCookiesEnabled() {
return mPrefs.getBoolean(Name.INCOGNITO_COOKIES, false);
}
public boolean getInvertColors() {
return mPrefs.getBoolean(Name.INVERT_COLORS, false);
}
public boolean getJavaScriptEnabled() {
return mPrefs.getBoolean(Name.JAVASCRIPT, true);
}
public boolean getLocationEnabled() {
return mPrefs.getBoolean(Name.LOCATION, false);
}
public boolean getOverviewModeEnabled() {
return mPrefs.getBoolean(Name.OVERVIEW_MODE, true);
}
public boolean getPopupsEnabled() {
return mPrefs.getBoolean(Name.POPUPS, true);
}
@NonNull
public String getProxyHost() {
return mPrefs.getString(Name.USE_PROXY_HOST, "localhost");
}
public int getProxyPort() {
return mPrefs.getInt(Name.USE_PROXY_PORT, 8118);
}
public int getReadingTextSize() {
return mPrefs.getInt(Name.READING_TEXT_SIZE, 2);
}
public int getRenderingMode() {
return mPrefs.getInt(Name.RENDERING_MODE, 0);
}
public boolean getRestoreLostTabsEnabled() {
return mPrefs.getBoolean(Name.RESTORE_LOST_TABS, true);
}
@Nullable
public String getSavedUrl() {
return mPrefs.getString(Name.SAVE_URL, null);
}
public boolean getSavePasswordsEnabled() {
return mPrefs.getBoolean(Name.SAVE_PASSWORDS, true);
}
public int getSearchChoice() {
return mPrefs.getInt(Name.SEARCH, 1);
}
@NonNull
public String getSearchUrl() {
return mPrefs.getString(Name.SEARCH_URL, Constants.GOOGLE_SEARCH);
}
public boolean getTextReflowEnabled() {
return mPrefs.getBoolean(Name.TEXT_REFLOW, false);
}
public int getTextSize() {
return mPrefs.getInt(Name.TEXT_SIZE, 3);
}
public int getUrlBoxContentChoice() {
return mPrefs.getInt(Name.URL_BOX_CONTENTS, 0);
}
public int getUseTheme() {
return mPrefs.getInt(Name.THEME, 0);
}
public boolean getUseProxy() {
return mPrefs.getBoolean(Name.USE_PROXY, false);
}
public int getProxyChoice() {
return mPrefs.getInt(Name.PROXY_CHOICE, Constants.NO_PROXY);
}
public int getUserAgentChoice() {
return mPrefs.getInt(Name.USER_AGENT, 1);
}
@Nullable
public String getUserAgentString(@Nullable String def) {
return mPrefs.getString(Name.USER_AGENT_STRING, def);
}
public boolean getUseWideViewportEnabled() {
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) {
return mPrefs.getBoolean(Name.SHOW_TABS_IN_DRAWER, defaultValue);
}
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(@NonNull String name, int value) {
mPrefs.edit().putInt(name, value).apply();
}
private void putString(@NonNull String name, @Nullable String value) {
mPrefs.edit().putString(name, value).apply();
}
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(@NonNull String encoding) {
putString(Name.TEXT_ENCODING, encoding);
}
public void setAdBlockEnabled(boolean enable) {
putBoolean(Name.BLOCK_ADS, enable);
}
public void setBlockImagesEnabled(boolean enable) {
putBoolean(Name.BLOCK_IMAGES, enable);
}
public void setBlockThirdPartyCookiesEnabled(boolean enable) {
putBoolean(Name.BLOCK_THIRD_PARTY, enable);
}
public void setCheckedForTor(boolean check) {
putBoolean(Name.INITIAL_CHECK_FOR_TOR, check);
}
public void setCheckedForI2P(boolean check) {
putBoolean(Name.INITIAL_CHECK_FOR_I2P, check);
}
public void setClearCacheExit(boolean enable) {
putBoolean(Name.CLEAR_CACHE_EXIT, enable);
}
public void setClearCookiesExitEnabled(boolean enable) {
putBoolean(Name.CLEAR_COOKIES_EXIT, enable);
}
public void setClearWebStorageExitEnabled(boolean enable) {
putBoolean(Name.CLEAR_WEBSTORAGE_EXIT, enable);
}
public void setClearHistoryExitEnabled(boolean enable) {
putBoolean(Name.CLEAR_HISTORY_EXIT, enable);
}
public void setColorModeEnabled(boolean enable) {
putBoolean(Name.ENABLE_COLOR_MODE, enable);
}
public void setCookiesEnabled(boolean enable) {
putBoolean(Name.COOKIES, enable);
}
public void setDownloadDirectory(@NonNull String directory) {
putString(Name.DOWNLOAD_DIRECTORY, directory);
}
public void setFlashSupport(int n) {
putInt(Name.ADOBE_FLASH_SUPPORT, n);
}
public void setFullScreenEnabled(boolean enable) {
putBoolean(Name.FULL_SCREEN, enable);
}
public void setGoogleSearchSuggestionsEnabled(boolean enabled) {
putBoolean(Name.GOOGLE_SEARCH_SUGGESTIONS, enabled);
}
public void setHideStatusBarEnabled(boolean enable) {
putBoolean(Name.HIDE_STATUS_BAR, enable);
}
public void setHomepage(@NonNull String homepage) {
putString(Name.HOMEPAGE, homepage);
}
public void setIncognitoCookiesEnabled(boolean enable) {
putBoolean(Name.INCOGNITO_COOKIES, enable);
}
public void setInvertColors(boolean enable) {
putBoolean(Name.INVERT_COLORS, enable);
}
public void setJavaScriptEnabled(boolean enable) {
putBoolean(Name.JAVASCRIPT, enable);
}
public void setLocationEnabled(boolean enable) {
putBoolean(Name.LOCATION, enable);
}
public void setOverviewModeEnabled(boolean enable) {
putBoolean(Name.OVERVIEW_MODE, enable);
}
public void setPopupsEnabled(boolean enable) {
putBoolean(Name.POPUPS, enable);
}
public void setReadingTextSize(int size) {
putInt(Name.READING_TEXT_SIZE, size);
}
public void setRenderingMode(int mode) {
putInt(Name.RENDERING_MODE, mode);
}
public void setRestoreLostTabsEnabled(boolean enable) {
putBoolean(Name.RESTORE_LOST_TABS, enable);
}
public void setSavedUrl(@Nullable String url) {
putString(Name.SAVE_URL, url);
}
public void setSavePasswordsEnabled(boolean enable) {
putBoolean(Name.SAVE_PASSWORDS, enable);
}
public void setSearchChoice(int choice) {
putInt(Name.SEARCH, choice);
}
public void setSearchUrl(@NonNull String url) {
putString(Name.SEARCH_URL, url);
}
public void setTextReflowEnabled(boolean enable) {
putBoolean(Name.TEXT_REFLOW, enable);
}
public void setTextSize(int size) {
putInt(Name.TEXT_SIZE, size);
}
public void setUrlBoxContentChoice(int choice) {
putInt(Name.URL_BOX_CONTENTS, choice);
}
public void setUseTheme(int theme) {
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>
* <li>{@link Constants#NO_PROXY}</li>
* <li>{@link Constants#PROXY_ORBOT}</li>
* <li>{@link Constants#PROXY_I2P}</li>
* </ul>
*
* @param choice the proxy to use.
*/
public void setProxyChoice(int choice) {
putBoolean(Name.USE_PROXY, choice != Constants.NO_PROXY);
putInt(Name.PROXY_CHOICE, choice);
}
public void setProxyHost(@NonNull String proxyHost) {
putString(Name.USE_PROXY_HOST, proxyHost);
}
public void setProxyPort(int proxyPort) {
putInt(Name.USE_PROXY_PORT, proxyPort);
}
public void setUserAgentChoice(int choice) {
putInt(Name.USER_AGENT, choice);
}
public void setUserAgentString(@Nullable String agent) {
putString(Name.USER_AGENT_STRING, agent);
}
public void setUseWideViewportEnabled(boolean enable) {
putBoolean(Name.USE_WIDE_VIEWPORT, enable);
}
}
@@ -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);
}
}
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
@@ -0,0 +1,246 @@
/*
* Copyright 2011 Peter Karich
*
* 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.reading;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Locale;
import acr.browser.lightning.constant.Constants;
/**
* This class is not thread safe. Use one new instance every time due to
* encoding variable.
*
* @author Peter Karich
*/
public class Converter {
private 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;
public Converter(String urlOnlyHint) {
url = urlOnlyHint;
}
public Converter() {
}
public Converter setMaxBytes(int maxBytes) {
this.maxBytes = maxBytes;
return this;
}
public static String extractEncoding(String contentType) {
String[] values;
if (contentType != null)
values = contentType.split(";");
else
values = new String[0];
String charset = "";
for (String value : values) {
value = value.trim().toLowerCase(Locale.getDefault());
if (value.startsWith("charset="))
charset = value.substring("charset=".length());
}
// http1.1 says ISO-8859-1 is the default charset
if (charset.isEmpty())
charset = ISO;
return charset;
}
public String getEncoding() {
if (encoding == null)
return "";
return encoding.toLowerCase(Locale.getDefault());
}
public String streamToString(InputStream is) {
return streamToString(is, maxBytes, encoding);
}
public String streamToString(InputStream is, String enc) {
return streamToString(is, maxBytes, enc);
}
/**
* reads bytes off the string and returns a string
*
* @param is input stream to read
* @param maxBytes
* The max bytes that we want to read from the input stream
* @return String
*/
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 ;)
if (encoding == null || encoding.isEmpty())
encoding = UTF8;
BufferedInputStream in = null;
try {
in = new BufferedInputStream(is, K2);
ByteArrayOutputStream output = new ByteArrayOutputStream();
// detect encoding with the help of meta tag
try {
in.mark(K2 * 2);
String tmpEnc = detectCharset("charset=", output, in, encoding);
if (tmpEnc != null)
encoding = tmpEnc;
else {
Log.d(Constants.TAG, "no charset found in first stage");
// detect with the help of xml beginning ala
// encoding="charset"
tmpEnc = detectCharset("encoding=", output, in, encoding);
if (tmpEnc != null)
encoding = tmpEnc;
else
Log.d(Constants.TAG, "no charset found in second stage");
}
if (!Charset.isSupported(encoding))
throw new UnsupportedEncodingException(encoding);
} catch (UnsupportedEncodingException e) {
Log.d(Constants.TAG,
"Using default encoding:" + UTF8 + " problem:" + e.getMessage()
+ " encoding:" + encoding + ' ' + url);
encoding = UTF8;
}
// SocketException: Connection reset
// IOException: missing CR => problem on server (probably some xml
// character thing?)
// IOException: Premature EOF => socket unexpectly closed from
// server
int bytesRead = output.size();
byte[] arr = new byte[K2];
while (true) {
if (bytesRead >= maxBytes) {
Log.d(Constants.TAG, "Maxbyte of " + maxBytes
+ " exceeded! Maybe html is now broken but try it nevertheless. Url: "
+ url);
break;
}
int n = in.read(arr);
if (n < 0)
break;
bytesRead += n;
output.write(arr, 0, n);
}
return output.toString(encoding);
} catch (IOException e) {
Log.e(Constants.TAG, e.toString() + " url:" + url);
} finally {
if (in != null) {
try {
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return "";
}
/**
* This method detects the charset even if the first call only returns some
* bytes. It will read until 4K bytes are reached and then try to determine
* the encoding
*
* @throws IOException
*/
private static String detectCharset(String key, ByteArrayOutputStream bos, BufferedInputStream in,
String enc) throws IOException {
// Grab better encoding from stream
byte[] arr = new byte[K2];
int nSum = 0;
while (nSum < K2) {
int n = in.read(arr);
if (n < 0)
break;
nSum += n;
bos.write(arr, 0, n);
}
String str = bos.toString(enc);
int encIndex = str.indexOf(key);
int clength = key.length();
if (encIndex > 0) {
char startChar = str.charAt(encIndex + clength);
int lastEncIndex;
if (startChar == '\'')
// if we have charset='something'
lastEncIndex = str.indexOf('\'', ++encIndex + clength);
else if (startChar == '\"')
// if we have charset="something"
lastEncIndex = str.indexOf('\"', ++encIndex + clength);
else {
// if we have "text/html; charset=utf-8"
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);
if (sec < 0)
sec = Integer.MAX_VALUE;
lastEncIndex = Math.min(first, sec);
// or "text/html; charset=utf-8 '
int third = str.indexOf('\'', encIndex + clength);
if (third > 0)
lastEncIndex = Math.min(lastEncIndex, third);
}
// re-read byte array with different encoding
// assume that the encoding string cannot be greater than 40 chars
if (lastEncIndex > encIndex + clength && lastEncIndex < encIndex + clength + 40) {
String tmpEnc = SHelper.encodingCleanup(str.substring(encIndex + clength,
lastEncIndex));
try {
in.reset();
bos.reset();
return tmpEnc;
} catch (IOException ex) {
Log.e(Constants.TAG, "Couldn't reset stream to re-read with new encoding "
+ tmpEnc + ' ' + ex.toString());
}
}
}
return null;
}
}
@@ -0,0 +1,483 @@
/*
* Copyright 2011 Peter Karich
*
* 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.reading;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.Proxy;
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.
*
* @author Peter Karich
*/
public class HtmlFetcher {
private static final Pattern SPACE = Pattern.compile(" ");
static {
SHelper.enableCookieMgmt();
SHelper.enableUserAgentOverwrite();
SHelper.enableAnySSL();
}
public static void main(String[] args) throws Exception {
BufferedReader reader = null;
BufferedWriter writer = null;
try {
//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);
}
}
private String referrer = "http://jetsli.de/crawler";
private String userAgent = "Mozilla/5.0 (compatible; Jetslide; +" + referrer + ')';
private String cacheControl = "max-age=0";
private String language = "en-us";
private String accept = "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5";
private String charset = "UTF-8";
private SCache cache;
private final AtomicInteger cacheCounter = new AtomicInteger(0);
private int maxTextLength = -1;
private ArticleTextExtractor extractor = new ArticleTextExtractor();
private Set<String> furtherResolveNecessary = new LinkedHashSet<String>() {
{
add("bit.ly");
add("cli.gs");
add("deck.ly");
add("fb.me");
add("feedproxy.google.com");
add("flic.kr");
add("fur.ly");
add("goo.gl");
add("is.gd");
add("ink.co");
add("j.mp");
add("lnkd.in");
add("on.fb.me");
add("ow.ly");
add("plurl.us");
add("sns.mx");
add("snurl.com");
add("su.pr");
add("t.co");
add("tcrn.ch");
add("tl.gd");
add("tiny.cc");
add("tinyurl.com");
add("tmi.me");
add("tr.im");
add("twurl.nl");
}
};
public HtmlFetcher() {
}
public void setExtractor(ArticleTextExtractor extractor) {
this.extractor = extractor;
}
public ArticleTextExtractor getExtractor() {
return extractor;
}
public HtmlFetcher setCache(SCache cache) {
this.cache = cache;
return this;
}
public SCache getCache() {
return cache;
}
public int getCacheCounter() {
return cacheCounter.get();
}
public HtmlFetcher clearCacheCounter() {
cacheCounter.set(0);
return this;
}
public HtmlFetcher setMaxTextLength(int maxTextLength) {
this.maxTextLength = maxTextLength;
return this;
}
public int getMaxTextLength() {
return maxTextLength;
}
public void setAccept(String accept) {
this.accept = accept;
}
public void setCharset(String charset) {
this.charset = charset;
}
public void setCacheControl(String cacheControl) {
this.cacheControl = cacheControl;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getReferrer() {
return referrer;
}
public HtmlFetcher setReferrer(String referrer) {
this.referrer = referrer;
return this;
}
public String getUserAgent() {
return userAgent;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
public String getAccept() {
return accept;
}
public String getCacheControl() {
return cacheControl;
}
public String getCharset() {
return charset;
}
public JResult fetchAndExtract(String url, int timeout, boolean resolve) throws Exception {
return fetchAndExtract(url, timeout, resolve, 0, false);
}
// main workhorse to call externally
@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);
if (gUrl != null)
url = gUrl;
else {
gUrl = SHelper.getUrlFromUglyFacebookRedirect(url);
if (gUrl != null)
url = gUrl;
}
if (resolve) {
// check if we can avoid resolving the URL (which hits the website!)
JResult res = getFromCache(url, originalUrl);
if (res != null)
return res;
String resUrl = getResolvedUrl(url, timeout, 0);
if (resUrl.isEmpty()) {
JResult result = new JResult();
if (cache != null)
cache.put(url, result);
return result.setUrl(url);
}
// if resolved url is different then use it!
if (!resUrl.equals(url)) {
// this is necessary e.g. for some homebaken url resolvers which return
// the resolved url relative to url!
url = SHelper.useDomainOfFirstArg4Second(url, resUrl);
}
}
// check if we have the (resolved) URL in cache
JResult res = getFromCache(url, originalUrl);
if (res != null)
return res;
JResult result = new JResult();
// or should we use? <link rel="canonical" href="http://www.N24.de/news/newsitem_6797232.html"/>
result.setUrl(url);
result.setOriginalUrl(originalUrl);
// Immediately put the url into the cache as extracting content takes time.
if (cache != null) {
cache.put(originalUrl, result);
cache.put(url, result);
}
// extract content to the extent appropriate for content type
String lowerUrl = url.toLowerCase();
if (SHelper.isDoc(lowerUrl) || SHelper.isApp(lowerUrl) || SHelper.isPackage(lowerUrl)) {
// skip
} else if (SHelper.isVideo(lowerUrl) || SHelper.isAudio(lowerUrl)) {
result.setVideoUrl(url);
} else if (SHelper.isImage(lowerUrl)) {
result.setImageUrl(url);
} else {
try {
String urlToDownload = url;
if (forceReload) {
urlToDownload = getURLtoBreakCache(url);
}
extractor.extractContent(result, fetchAsString(urlToDownload, timeout), maxContentSize);
} catch (IOException io) {
// do nothing
}
if (result.getFaviconUrl().isEmpty())
result.setFaviconUrl(SHelper.getDefaultFavicon(url));
// some links are relative to root and do not include the domain of the url :(
if (!result.getFaviconUrl().isEmpty())
result.setFaviconUrl(fixUrl(url, result.getFaviconUrl()));
if (!result.getImageUrl().isEmpty())
result.setImageUrl(fixUrl(url, result.getImageUrl()));
if (!result.getVideoUrl().isEmpty())
result.setVideoUrl(fixUrl(url, result.getVideoUrl()));
if (!result.getRssUrl().isEmpty())
result.setRssUrl(fixUrl(url, result.getRssUrl()));
}
result.setText(lessText(result.getText()));
synchronized (result) {
result.notifyAll();
}
return result;
}
// Ugly hack to break free from any cached versions, a few URLs required this.
private static String getURLtoBreakCache(String url) {
try {
URL aURL = new URL(url);
if (aURL.getQuery() != null && aURL.getQuery().isEmpty()) {
return url + "?1";
} else {
return url + "&1";
}
} catch (MalformedURLException e) {
return url;
}
}
private String lessText(String text) {
if (text == null)
return "";
if (maxTextLength >= 0 && text.length() > maxTextLength)
return text.substring(0, maxTextLength);
return text;
}
private static String fixUrl(String url, String urlOrPath) {
return SHelper.useDomainOfFirstArg4Second(url, urlOrPath);
}
private String fetchAsString(String urlAsString, int timeout)
throws IOException {
return fetchAsString(urlAsString, timeout, true);
}
// main routine to get raw webpage content
private String fetchAsString(String urlAsString, int timeout, boolean includeSomeGooseOptions)
throws IOException {
HttpURLConnection hConn = createUrlConnection(urlAsString, timeout, includeSomeGooseOptions);
hConn.setInstanceFollowRedirects(true);
String encoding = hConn.getContentEncoding();
InputStream is;
if ("gzip".equalsIgnoreCase(encoding)) {
is = new GZIPInputStream(hConn.getInputStream());
} else if ("deflate".equalsIgnoreCase(encoding)) {
is = new InflaterInputStream(hConn.getInputStream(), new Inflater(true));
} else {
is = hConn.getInputStream();
}
String enc = Converter.extractEncoding(hConn.getContentType());
return createConverter(urlAsString).streamToString(is, enc);
}
private static Converter createConverter(String url) {
return new Converter(url);
}
/**
* On some devices we have to hack:
* http://developers.sun.com/mobility/reference/techart/design_guidelines/http_redirection.html
*
* @param timeout Sets a specified timeout value, in milliseconds
* @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
*/
private String getResolvedUrl(String urlAsString, int timeout,
int num_redirects) {
String newUrl = null;
int responseCode = -1;
try {
HttpURLConnection hConn = createUrlConnection(urlAsString, timeout, true);
// force no follow
hConn.setInstanceFollowRedirects(false);
// the program doesn't care what the content actually is !!
// http://java.sun.com/developer/JDCTechTips/2003/tt0422.html
hConn.setRequestMethod("HEAD");
hConn.connect();
responseCode = hConn.getResponseCode();
hConn.getInputStream().close();
if (responseCode == HttpURLConnection.HTTP_OK)
return urlAsString;
newUrl = hConn.getHeaderField("Location");
// Note that the max recursion level is 5.
if (responseCode / 100 == 3 && newUrl != null && num_redirects < 5) {
newUrl = SPACE.matcher(newUrl).replaceAll("+");
// some services use (none-standard) utf8 in their location header
if (urlAsString.contains("://bit.ly")
|| urlAsString.contains("://is.gd"))
newUrl = encodeUriFromHeader(newUrl);
// AP: This code is not longer need, instead we always follow
// multiple redirects.
//
// fix problems if shortened twice. as it is often the case after twitters' t.co bullshit
//if (furtherResolveNecessary.contains(SHelper.extractDomain(newUrl, true)))
// newUrl = getResolvedUrl(newUrl, timeout);
// Add support for URLs with multiple levels of redirection,
// call getResolvedUrl until there is no more redirects or a
// max number of redirects is reached.
newUrl = SHelper.useDomainOfFirstArg4Second(urlAsString, newUrl);
newUrl = getResolvedUrl(newUrl, timeout, num_redirects + 1);
return newUrl;
} else
return urlAsString;
} catch (Exception ex) {
return "";
}
}
/**
* Takes a URI that was decoded as ISO-8859-1 and applies percent-encoding
* to non-ASCII characters. Workaround for broken origin servers that send
* UTF-8 in the Location: header.
*/
private static String encodeUriFromHeader(String badLocation) {
StringBuilder sb = new StringBuilder(badLocation.length());
for (char ch : badLocation.toCharArray()) {
if (ch < (char) 128) {
sb.append(ch);
} else {
// this is ONLY valid if the uri was decoded using ISO-8859-1
sb.append(String.format("%%%02X", (int) ch));
}
}
return sb.toString();
}
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);
hConn.setRequestProperty("User-Agent", userAgent);
hConn.setRequestProperty("Accept", accept);
if (includeSomeGooseOptions) {
hConn.setRequestProperty("Accept-Language", language);
hConn.setRequestProperty("content-charset", charset);
hConn.addRequestProperty("Referer", referrer);
// avoid the cache for testing purposes only?
hConn.setRequestProperty("Cache-Control", cacheControl);
}
// suggest respond to be gzipped or deflated (which is just another compression)
// http://stackoverflow.com/q/3932117
hConn.setRequestProperty("Accept-Encoding", "gzip, deflate");
hConn.setConnectTimeout(timeout);
hConn.setReadTimeout(timeout);
return hConn;
}
private JResult getFromCache(String url, String originalUrl) {
if (cache != null) {
JResult res = cache.get(url);
if (res != null) {
// e.g. the cache returned a shortened url as original url now we want to store the
// current original url! Also it can be that the cache response to url but the JResult
// does not contain it so overwrite it:
res.setUrl(url);
res.setOriginalUrl(originalUrl);
cacheCounter.addAndGet(1);
return res;
}
}
return null;
}
}
@@ -0,0 +1,31 @@
package acr.browser.lightning.reading;
import org.jsoup.nodes.Element;
/**
* Class which encapsulates the data from an image found under an element
*
* @author Chris Alexander, chris@chris-alexander.co.uk
*/
class ImageResult {
private final String src;
public final Integer weight;
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,
boolean noFollow) {
this.src = src;
this.weight = weight;
this.title = title;
this.height = height;
this.width = width;
this.alt = alt;
this.noFollow = noFollow;
}
}
@@ -0,0 +1,274 @@
/*
* Copyright 2011 Peter Karich
*
* 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.reading;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* Parsed result from web page containing important title, text and image.
*
* @author Peter Karich
*/
public class JResult implements Serializable {
private String title;
private String url;
private String originalUrl;
private String canonicalUrl;
private String imageUrl;
private String videoUrl;
private String rssUrl;
private String text;
private String faviconUrl;
private String description;
private String authorName;
private String authorDescription;
private Date date;
private Collection<String> keywords;
private List<ImageResult> images = null;
private final List<Map<String, String>> links = new ArrayList<>();
private String type;
private String sitename;
private String language;
public JResult() {
}
public String getUrl() {
if (url == null)
return "";
return url;
}
public JResult setUrl(String url) {
this.url = url;
return this;
}
public JResult setOriginalUrl(String originalUrl) {
this.originalUrl = originalUrl;
return this;
}
public String getOriginalUrl() {
return originalUrl;
}
public JResult setCanonicalUrl(String canonicalUrl) {
this.canonicalUrl = canonicalUrl;
return this;
}
public String getCanonicalUrl() {
return canonicalUrl;
}
public String getFaviconUrl() {
if (faviconUrl == null)
return "";
return faviconUrl;
}
public JResult setFaviconUrl(String faviconUrl) {
this.faviconUrl = faviconUrl;
return this;
}
public JResult setRssUrl(String rssUrl) {
this.rssUrl = rssUrl;
return this;
}
public String getRssUrl() {
if (rssUrl == null)
return "";
return rssUrl;
}
public String getDescription() {
if (description == null)
return "";
return description;
}
public JResult setDescription(String description) {
this.description = description;
return this;
}
public String getAuthorName() {
if (authorName == null)
return "";
return authorName;
}
public JResult setAuthorName(String authorName) {
this.authorName = authorName;
return this;
}
public String getAuthorDescription() {
if (authorDescription == null)
return "";
return authorDescription;
}
public JResult setAuthorDescription(String authorDescription) {
this.authorDescription = authorDescription;
return this;
}
public String getImageUrl() {
if (imageUrl == null)
return "";
return imageUrl;
}
public JResult setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
return this;
}
public String getText() {
if (text == null)
return "";
return text;
}
public JResult setText(String text) {
this.text = text;
return this;
}
public String getTitle() {
if (title == null)
return "";
return title;
}
public JResult setTitle(String title) {
this.title = title;
return this;
}
public String getVideoUrl() {
if (videoUrl == null)
return "";
return videoUrl;
}
public JResult setVideoUrl(String videoUrl) {
this.videoUrl = videoUrl;
return this;
}
public JResult setDate(Date date) {
this.date = date;
return this;
}
public Collection<String> getKeywords() {
return keywords;
}
public void setKeywords(Collection<String> keywords) {
this.keywords = keywords;
}
/**
* @return get date from url or guessed from text
*/
public Date getDate() {
return date;
}
/**
* @return images list
*/
public List<ImageResult> getImages() {
if (images == null)
return Collections.emptyList();
return images;
}
/**
* @return images count
*/
public int getImagesCount() {
if (images == null)
return 0;
return images.size();
}
/**
* set images list
*/
public void setImages(List<ImageResult> images) {
this.images = images;
}
public void addLink(String url, String text, Integer pos) {
Map<String, String> link = new HashMap<>();
link.put("url", url);
link.put("text", text);
link.put("offset", String.valueOf(pos));
links.add(link);
}
public List<Map<String, String>> getLinks() {
if (links == null)
return Collections.emptyList();
return links;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getSitename() {
return sitename;
}
public void setSitename(String sitename) {
this.sitename = sitename;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
@Override
public String toString() {
return "title:" + getTitle() + " imageUrl:" + getImageUrl() + " text:" + text;
}
}
@@ -0,0 +1,78 @@
/**
* 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
* License for the specific language governing permissions and limitations under
* the License.
*/
package acr.browser.lightning.reading;
import java.io.Serializable;
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
*/
public class MapEntry<K, V> implements Map.Entry<K, V>, Serializable {
private static final long serialVersionUID = 1L;
private final K key;
private V value;
public MapEntry(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
@Override
public V setValue(V value) {
this.value = value;
return value;
}
@Override
public String toString() {
return key + ", " + value;
}
@Override
public boolean equals(Object obj) {
if (obj == null)
return false;
if (!(obj instanceof Map<?, ?>))
return false;
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
public int hashCode() {
int hash = 7;
hash = 19 * hash + (this.key != null ? this.key.hashCode() : 0);
hash = 19 * hash + (this.value != null ? this.value.hashCode() : 0);
return hash;
}
}
@@ -0,0 +1,216 @@
package acr.browser.lightning.reading;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
/**
* @author goose | jim
* @author karussell
* <p/>
* this class will be responsible for taking our top node and stripping out junk
* we don't want and getting it ready for how we want it presented to the user
*/
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 final int minFirstParagraphText;
private final int minParagraphText;
private final List<String> nodesToReplace;
private String nodesToKeepCssSelector = "p, ol";
public OutputFormatter() {
this(MIN_FIRST_PARAGRAPH_TEXT, MIN_PARAGRAPH_TEXT, NODES_TO_REPLACE);
}
public OutputFormatter(int minParagraphText) {
this(minParagraphText, minParagraphText, NODES_TO_REPLACE);
}
public OutputFormatter(int minFirstParagraphText, int minParagraphText) {
this(minFirstParagraphText, minParagraphText, NODES_TO_REPLACE);
}
private OutputFormatter(int minFirstParagraphText, int minParagraphText,
List<String> nodesToReplace) {
this.minFirstParagraphText = minFirstParagraphText;
this.minParagraphText = minParagraphText;
this.nodesToReplace = nodesToReplace;
}
/**
* set elements to keep in output text
*/
public void setNodesToKeepCssSelector(String nodesToKeepCssSelector) {
this.nodesToKeepCssSelector = nodesToKeepCssSelector;
}
/**
* takes an element and turns the P tags into \n\n
*/
public String getFormattedText(Element topNode) {
setParagraphIndex(topNode, nodesToKeepCssSelector);
removeNodesWithNegativeScores(topNode);
StringBuilder sb = new StringBuilder();
int countOfP = append(topNode, sb, nodesToKeepCssSelector);
String str = SHelper.innerTrim(sb.toString());
int topNodeLength = topNode.text().length();
if (topNodeLength == 0) {
topNodeLength = 1;
}
boolean lowTextRatio = ((str.length() / (topNodeLength * 1.0)) < 0.25);
if (str.length() > 100 && countOfP > 0 && !lowTextRatio)
return str;
// no subelements
if (str.isEmpty() || (!topNode.text().isEmpty()
&& str.length() <= topNode.ownText().length())
|| countOfP == 0 || lowTextRatio) {
str = topNode.text();
}
// if jsoup failed to parse the whole html now parse this smaller
// snippet again to avoid html tags disturbing our text:
return Jsoup.parse(str).text();
}
/**
* If there are elements inside our top node that have a negative gravity
* score remove them
*/
private void removeNodesWithNegativeScores(Element topNode) {
Elements gravityItems = topNode.select("*[gravityScore]");
for (Element item : gravityItems) {
int score = getScore(item);
int paragraphIndex = getParagraphIndex(item);
if (score < 0 || item.text().length() < getMinParagraph(paragraphIndex)) {
item.remove();
}
}
}
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?
MAIN:
for (Element e : node.select(tagName)) {
Element tmpEl = e;
// check all elements until 'node'
while (tmpEl != null && !tmpEl.equals(node)) {
if (unlikely(tmpEl))
continue MAIN;
tmpEl = tmpEl.parent();
}
String text = node2Text(e);
if (text.isEmpty() || text.length() < getMinParagraph(paragraphWithTextIndex)
|| text.length() > SHelper.countLetters(text) * 2) {
continue;
}
if (e.tagName().equals("p")) {
countOfP++;
}
sb.append(text);
sb.append("\n\n");
paragraphWithTextIndex += 1;
}
return countOfP;
}
private static void setParagraphIndex(Element node, String tagName) {
int paragraphIndex = 0;
for (Element e : node.select(tagName)) {
e.attr("paragraphIndex", Integer.toString(paragraphIndex++));
}
}
private int getMinParagraph(int paragraphIndex) {
if (paragraphIndex < 1) {
return minFirstParagraphText;
} else {
return minParagraphText;
}
}
private static int getParagraphIndex(Element el) {
try {
return Integer.parseInt(el.attr("paragraphIndex"));
} catch (NumberFormatException ex) {
return -1;
}
}
private static int getScore(Element el) {
try {
return Integer.parseInt(el.attr("gravityScore"));
} catch (Exception ex) {
return 0;
}
}
private boolean unlikely(Node e) {
if (e.attr("class") != null && e.attr("class").toLowerCase().contains("caption"))
return true;
String style = e.attr("style");
String clazz = e.attr("class");
return unlikelyPattern.matcher(style).find() || unlikelyPattern.matcher(clazz).find();
}
private void appendTextSkipHidden(Element e, StringBuilder accum, int indent) {
for (Node child : e.childNodes()) {
if (unlikely(child)) {
continue;
}
if (child instanceof TextNode) {
TextNode textNode = (TextNode) child;
String txt = textNode.text();
accum.append(txt);
} else if (child instanceof Element) {
Element element = (Element) child;
if (accum.length() > 0 && element.isBlock()
&& !lastCharIsWhitespace(accum))
accum.append(' ');
else if (element.tagName().equals("br"))
accum.append(' ');
appendTextSkipHidden(element, accum, indent + 1);
}
}
}
private static boolean lastCharIsWhitespace(StringBuilder accum) {
return accum.length() != 0 && Character.isWhitespace(accum.charAt(accum.length() - 1));
}
private String node2Text(Element el) {
StringBuilder sb = new StringBuilder(200);
appendTextSkipHidden(el, sb, 0);
return sb.toString();
}
private OutputFormatter setUnlikelyPattern(String unlikelyPattern) {
this.unlikelyPattern = Pattern.compile(unlikelyPattern);
return this;
}
public OutputFormatter appendUnlikelyPattern(String str) {
return setUnlikelyPattern(unlikelyPattern.toString() + '|' + str);
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2011 Peter Karich
*
* 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.reading;
/**
*
* @author Peter Karich
*/
public interface SCache {
JResult get(String url);
void put(String url, JResult res);
int getSize();
}
@@ -0,0 +1,442 @@
/*
* Copyright 2011 Peter Karich
*
* 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.reading;
import org.jsoup.nodes.Element;
import java.io.UnsupportedEncodingException;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
/**
* @author Peter Karich
*/
class SHelper {
private static final String UTF8 = "UTF-8";
private static final Pattern SPACE = Pattern.compile(" ");
public static String replaceSpaces(String url) {
if (!url.isEmpty()) {
url = url.trim();
if (url.contains(" ")) {
Matcher spaces = SPACE.matcher(url);
url = spaces.replaceAll("%20");
}
}
return url;
}
public static int count(String str, String substring) {
int c = 0;
int index1 = str.indexOf(substring);
if (index1 >= 0) {
c++;
c += count(str.substring(index1 + substring.length()), substring);
}
return c;
}
/**
* remove more than two spaces or newlines
*/
public static String innerTrim(String str) {
if (str.isEmpty())
return "";
StringBuilder sb = new StringBuilder(str.length());
boolean previousSpace = false;
for (int i = 0, length = str.length(); i < length; i++) {
char c = str.charAt(i);
if (c == ' ' || (int) c == 9 || c == '\n') {
previousSpace = true;
continue;
}
if (previousSpace)
sb.append(' ');
previousSpace = false;
sb.append(c);
}
return sb.toString().trim();
}
/**
* Starts reading the encoding from the first valid character until an
* invalid encoding character occurs.
*/
public static String encodingCleanup(String str) {
StringBuilder sb = new StringBuilder(str.length());
boolean startedWithCorrectString = false;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (Character.isDigit(c) || Character.isLetter(c) || c == '-' || c == '_') {
startedWithCorrectString = true;
sb.append(c);
continue;
}
if (startedWithCorrectString)
break;
}
return sb.toString().trim();
}
/**
* @return the longest substring as str1.substring(result[0], result[1]);
*/
public static String getLongestSubstring(String str1, String str2) {
int res[] = longestSubstring(str1, str2);
if (res == null || res[0] >= res[1])
return "";
return str1.substring(res[0], res[1]);
}
private static int[] longestSubstring(String str1, String str2) {
if (str1 == null || str1.isEmpty() || str2 == null || str2.isEmpty())
return null;
// dynamic programming => save already identical length into array
// to understand this algo simply print identical length in every entry of the array
// i+1, j+1 then reuses information from i,j
// java initializes them already with 0
int[][] num = new int[str1.length()][str2.length()];
int maxlen = 0;
int lastSubstrBegin = 0;
int endIndex = 0;
for (int i = 0; i < str1.length(); i++) {
for (int j = 0; j < str2.length(); j++) {
if (str1.charAt(i) == str2.charAt(j)) {
if ((i == 0) || (j == 0))
num[i][j] = 1;
else
num[i][j] = 1 + num[i - 1][j - 1];
if (num[i][j] > maxlen) {
maxlen = num[i][j];
// generate substring from str1 => i
lastSubstrBegin = i - num[i][j] + 1;
endIndex = i + 1;
}
}
}
}
return new int[]{lastSubstrBegin, endIndex};
}
public static String getDefaultFavicon(String url) {
return useDomainOfFirstArg4Second(url, "/favicon.ico");
}
/**
* @param urlForDomain extract the domain from this url
* @param path this url does not have a domain
* @return
*/
public static String useDomainOfFirstArg4Second(String urlForDomain, String path) {
try {
// See: http://stackoverflow.com/questions/1389184/building-an-absolute-url-from-a-relative-url-in-java
URL baseUrl = new URL(urlForDomain);
URL relativeurl = new URL(baseUrl, path);
return relativeurl.toString();
} catch (MalformedURLException ex) {
return path;
}
}
public static String extractHost(String url) {
return extractDomain(url, false);
}
public static String extractDomain(String url, boolean aggressive) {
if (url.startsWith("http://"))
url = url.substring("http://".length());
else if (url.startsWith("https://"))
url = url.substring("https://".length());
if (aggressive) {
if (url.startsWith("www."))
url = url.substring("www.".length());
// strip mobile from start
if (url.startsWith("m."))
url = url.substring("m.".length());
}
int slashIndex = url.indexOf('/');
if (slashIndex > 0)
url = url.substring(0, slashIndex);
return url;
}
public static boolean isVideoLink(String url) {
url = extractDomain(url, true);
return url.startsWith("youtube.com") || url.startsWith("video.yahoo.com")
|| url.startsWith("vimeo.com") || url.startsWith("blip.tv");
}
public static boolean isVideo(String url) {
return url.endsWith(".mpeg") || url.endsWith(".mpg") || url.endsWith(".avi") || url.endsWith(".mov")
|| url.endsWith(".mpg4") || url.endsWith(".mp4") || url.endsWith(".flv") || url.endsWith(".wmv");
}
public static boolean isAudio(String url) {
return url.endsWith(".mp3") || url.endsWith(".ogg") || url.endsWith(".m3u") || url.endsWith(".wav");
}
public static boolean isDoc(String url) {
return url.endsWith(".pdf") || url.endsWith(".ppt") || url.endsWith(".doc")
|| url.endsWith(".swf") || url.endsWith(".rtf") || url.endsWith(".xls");
}
public static boolean isPackage(String url) {
return url.endsWith(".gz") || url.endsWith(".tgz") || url.endsWith(".zip")
|| url.endsWith(".rar") || url.endsWith(".deb") || url.endsWith(".rpm") || url.endsWith(".7z");
}
public static boolean isApp(String url) {
return url.endsWith(".exe") || url.endsWith(".bin") || url.endsWith(".bat") || url.endsWith(".dmg");
}
public static boolean isImage(String url) {
return url.endsWith(".png") || url.endsWith(".jpeg") || url.endsWith(".gif")
|| url.endsWith(".jpg") || url.endsWith(".bmp") || url.endsWith(".ico") || url.endsWith(".eps");
}
/**
* @see "http://blogs.sun.com/CoreJavaTechTips/entry/cookie_handling_in_java_se"
*/
public static void enableCookieMgmt() {
CookieManager manager = new CookieManager();
manager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
CookieHandler.setDefault(manager);
}
/**
* @see "http://stackoverflow.com/questions/2529682/setting-user-agent-of-a-java-urlconnection"
*/
public static void enableUserAgentOverwrite() {
System.setProperty("http.agent", "");
}
public static String getUrlFromUglyGoogleRedirect(String url) {
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());
}
}
return null;
}
public static String getUrlFromUglyFacebookRedirect(String url) {
if (url.startsWith("https://www.facebook.com/l.php?u=")) {
url = url.substring("https://www.facebook.com/l.php?u=".length());
return urlDecode(url);
}
return null;
}
public static String urlEncode(String str) {
try {
return URLEncoder.encode(str, UTF8);
} catch (UnsupportedEncodingException ex) {
return str;
}
}
private static String urlDecode(String str) {
try {
return URLDecoder.decode(str, UTF8);
} catch (UnsupportedEncodingException ex) {
return str;
}
}
/**
* Popular sites uses the #! to indicate the importance of the following
* chars. Ugly but true. Such as: facebook, twitter, gizmodo, ...
*/
public static String removeHashbang(String url) {
return url.replaceFirst("#!", "");
}
public static String printNode(Element root) {
return printNode(root, 0);
}
private static String printNode(Element root, int indentation) {
StringBuilder sb = new StringBuilder(indentation);
for (int i = 0; i < indentation; i++) {
sb.append(' ');
}
sb.append(root.tagName());
sb.append(':');
sb.append(root.ownText());
sb.append('\n');
for (Element el : root.children()) {
sb.append(printNode(el, indentation + 1));
sb.append('\n');
}
return sb.toString();
}
public static String estimateDate(String url) {
int index = url.indexOf("://");
if (index > 0)
url = url.substring(index + 3);
int year = -1;
int yearCounter = -1;
int month = -1;
int monthCounter = -1;
int day = -1;
String strs[] = url.split("/");
for (int counter = 0; counter < strs.length; counter++) {
String str = strs[counter];
if (str.length() == 4) {
try {
year = Integer.parseInt(str);
} catch (Exception ex) {
continue;
}
if (year < 1970 || year > 3000) {
year = -1;
continue;
}
yearCounter = counter;
} else if (str.length() == 2) {
if (monthCounter < 0 && counter == yearCounter + 1) {
try {
month = Integer.parseInt(str);
} catch (Exception ex) {
continue;
}
if (month < 1 || month > 12) {
month = -1;
continue;
}
monthCounter = counter;
} else if (counter == monthCounter + 1) {
try {
day = Integer.parseInt(str);
} catch (Exception ignored) {
// ignored
}
if (day < 1 || day > 31) {
day = -1;
continue;
}
break;
}
}
}
if (year < 0)
return null;
StringBuilder str = new StringBuilder(year);
if (month < 1)
return str.toString();
str.append('/');
if (month < 10)
str.append('0');
str.append(month);
if (day < 1)
return str.toString();
str.append('/');
if (day < 10)
str.append('0');
str.append(day);
return str.toString();
}
public static String completeDate(String dateStr) {
if (dateStr == null)
return null;
int index = dateStr.indexOf('/');
if (index > 0) {
index = dateStr.indexOf('/', index + 1);
if (index > 0)
return dateStr;
else
return dateStr + "/01";
}
return dateStr + "/01/01";
}
// with the help of http://stackoverflow.com/questions/1828775/httpclient-and-ssl
public static void enableAnySSL() {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(new KeyManager[0], new TrustManager[]{new DefaultTrustManager()}, new SecureRandom());
SSLContext.setDefault(ctx);
} catch (Exception ex) {
ex.printStackTrace();
}
}
private static class DefaultTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
public static int countLetters(String str) {
int len = str.length();
int chars = 0;
for (int i = 0; i < len; i++) {
if (Character.isLetter(str.charAt(i)))
chars++;
}
return chars;
}
}
@@ -0,0 +1,22 @@
package acr.browser.lightning.receiver;
import android.content.BroadcastReceiver;
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 static boolean isConnected(@NonNull Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null)
return false;
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
return activeNetwork != null && activeNetwork.isConnected();
}
}
@@ -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);
}
@@ -0,0 +1,184 @@
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;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
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";
private static final String BLOCKED_DOMAINS_LIST_FILE_NAME = "hosts.txt";
private static final String LOCAL_IP_V4 = "127.0.0.1";
private static final String LOCAL_IP_V4_ALT = "0.0.0.0";
private static final String LOCAL_IP_V6 = "::1";
private static final String LOCALHOST = "localhost";
private static final String COMMENT = "#";
private static final String TAB = "\t";
private static final String SPACE = " ";
private static final String EMPTY = "";
private final Set<String> mBlockedDomainsList = new HashSet<>();
private boolean mBlockAds;
private static final Locale mLocale = Locale.getDefault();
@Inject PreferenceManager mPreferenceManager;
@Inject
public AdBlock(@NonNull Context context) {
BrowserApp.getAppComponent().inject(this);
if (mBlockedDomainsList.isEmpty() && Constants.FULL_VERSION) {
loadHostsFile(context);
}
mBlockAds = mPreferenceManager.getAdBlockEnabled();
}
public void updatePreference() {
mBlockAds = mPreferenceManager.getAdBlockEnabled();
}
private void loadBlockedDomainsList(@NonNull final Context context) {
BrowserApp.getTaskThread().execute(new Runnable() {
@Override
public void run() {
AssetManager asset = context.getAssets();
BufferedReader reader = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
reader = new BufferedReader(new InputStreamReader(
asset.open(BLOCKED_DOMAINS_LIST_FILE_NAME)));
String line;
while ((line = reader.readLine()) != null) {
mBlockedDomainsList.add(line.trim().toLowerCase(mLocale));
}
} catch (IOException e) {
Log.wtf(TAG, "Reading blocked domains list from file '"
+ BLOCKED_DOMAINS_LIST_FILE_NAME + "' failed.", e);
} finally {
Utils.close(reader);
}
}
});
}
/**
* a method that determines if the given URL is an ad or not. It performs
* a search of the URL's domain on the blocked domain hash set.
*
* @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(@Nullable String url) {
if (!mBlockAds || url == null) {
return false;
}
String domain;
try {
domain = getDomainName(url);
} catch (URISyntaxException e) {
Log.d(TAG, "URL '" + url + "' is invalid", e);
return false;
}
boolean isOnBlacklist = mBlockedDomainsList.contains(domain.toLowerCase(mLocale));
if (isOnBlacklist) {
Log.d(TAG, "URL '" + url + "' is an ad");
}
return isOnBlacklist;
}
/**
* 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
*/
@NonNull
private static String getDomainName(@NonNull String url) throws URISyntaxException {
int index = url.indexOf('/', 8);
if (index != -1) {
url = url.substring(0, index);
}
URI uri = new URI(url);
String domain = uri.getHost();
if (domain == null) {
return url;
}
return domain.startsWith("www.") ? domain.substring(4) : domain;
}
/**
* This method reads through a hosts file and extracts the domains that should
* be redirected to localhost (a.k.a. IP address 127.0.0.1). It can handle files that
* simply have a list of hostnames to block, or it can handle a full blown hosts file.
* 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(@NonNull final Context context) {
BrowserApp.getTaskThread().execute(new Runnable() {
@Override
public void run() {
AssetManager asset = context.getAssets();
BufferedReader reader = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
reader = new BufferedReader(new InputStreamReader(
asset.open(BLOCKED_DOMAINS_LIST_FILE_NAME)));
String line;
while ((line = reader.readLine()) != null) {
if (!line.isEmpty() && !line.startsWith(COMMENT)) {
line = line.replace(LOCAL_IP_V4, EMPTY)
.replace(LOCAL_IP_V4_ALT, EMPTY)
.replace(LOCAL_IP_V6, EMPTY)
.replace(TAB, EMPTY);
int comment = line.indexOf(COMMENT);
if (comment >= 0) {
line = line.substring(0, comment);
}
line = line.trim();
if (!line.isEmpty() && !line.equals(LOCALHOST)) {
while (line.contains(SPACE)) {
int space = line.indexOf(SPACE);
String host = line.substring(0, space);
mBlockedDomainsList.add(host.trim());
line = line.substring(space, line.length()).trim();
}
mBlockedDomainsList.add(line.trim());
}
}
}
} catch (IOException e) {
Log.wtf(TAG, "Reading blocked domains list from file '"
+ BLOCKED_DOMAINS_LIST_FILE_NAME + "' failed.", e);
} finally {
Utils.close(reader);
}
}
});
}
}
@@ -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;
}
}
@@ -0,0 +1,113 @@
package acr.browser.lightning.utils;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
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;
import java.net.URISyntaxException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import acr.browser.lightning.constant.Constants;
public class IntentUtils {
private final Activity mActivity;
private static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile("(?i)"
+ // switch on case insensitive matching
'('
+ // begin group for schema
"(?:http|https|file)://" + "|(?:inline|data|about|javascript):" + "|(?:.*:.*@)"
+ ')' + "(.*)");
public IntentUtils(Activity activity) {
mActivity = activity;
}
public boolean startActivityForUrl(@Nullable WebView tab, @NonNull String url) {
Intent intent;
try {
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
} catch (URISyntaxException ex) {
Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
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) {
intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://search?q=pname:"
+ packagename));
intent.addCategory(Intent.CATEGORY_BROWSABLE);
mActivity.startActivity(intent);
return true;
} else {
return false;
}
}
if (tab != null) {
intent.putExtra(Constants.INTENT_ORIGIN, 1);
}
Matcher m = ACCEPTED_URI_SCHEMA.matcher(url);
if (m.matches() && !isSpecializedHandlerAvailable(intent)) {
return false;
}
try {
if (mActivity.startActivityIfNeeded(intent, -1)) {
return true;
}
} catch (ActivityNotFoundException ex) {
ex.printStackTrace();
}
return false;
}
/**
* Search for intent handlers that are specific to this URL aka, specialized
* apps like google maps or youtube
*/
private boolean isSpecializedHandlerAvailable(Intent intent) {
PackageManager pm = mActivity.getPackageManager();
List<ResolveInfo> handlers = pm.queryIntentActivities(intent,
PackageManager.GET_RESOLVED_FILTER);
if (handlers == null || handlers.isEmpty()) {
return false;
}
for (ResolveInfo resolveInfo : handlers) {
IntentFilter filter = resolveInfo.filter;
if (filter == null) {
// No intent filter matches this intent?
// Error on the side of staying in the browser, ignore
continue;
}
// NOTICE: Use of && instead of || will cause the browser
// to launch a new intent for every URL, using OR only
// launches a new one if there is a non-browser app that
// can handle it.
if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) {
// Generic handler, skip
continue;
}
return true;
}
return false;
}
}
@@ -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) {}
}
}
@@ -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");
}
}
}
@@ -0,0 +1,212 @@
package acr.browser.lightning.utils;
import android.app.Activity;
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.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;
@Singleton
public class ProxyUtils {
// Helper
private static boolean mI2PHelperBound;
private static boolean mI2PProxyInitialized;
@Inject PreferenceManager mPreferences;
@Inject I2PAndroidHelper mI2PHelper;
@Inject Bus mBus;
@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(@NonNull final Activity activity) {
boolean useProxy = mPreferences.getUseProxy();
final boolean orbotInstalled = OrbotHelper.isOrbotInstalled(activity);
boolean orbotChecked = mPreferences.getCheckedForTor();
boolean orbot = orbotInstalled && !orbotChecked;
boolean i2pInstalled = mI2PHelper.isI2PAndroidInstalled();
boolean i2pChecked = mPreferences.getCheckedForI2P();
boolean i2p = i2pInstalled && !i2pChecked;
// TODO Is the idea to show this per-session, or only once?
if (!useProxy && (orbot || i2p)) {
if (orbot) mPreferences.setCheckedForTor(true);
if (i2p) mPreferences.setCheckedForI2P(true);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
if (orbotInstalled && i2pInstalled) {
String[] proxyChoices = activity.getResources().getStringArray(R.array.proxy_choices_array);
builder.setTitle(activity.getResources().getString(R.string.http_proxy))
.setSingleChoiceItems(proxyChoices, mPreferences.getProxyChoice(),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setProxyChoice(which);
}
})
.setNeutralButton(activity.getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (mPreferences.getUseProxy())
initializeProxy(activity);
}
});
} else {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
mPreferences.setProxyChoice(orbotInstalled ?
Constants.PROXY_ORBOT : Constants.PROXY_I2P);
initializeProxy(activity);
break;
case DialogInterface.BUTTON_NEGATIVE:
mPreferences.setProxyChoice(Constants.NO_PROXY);
break;
}
}
};
builder.setMessage(orbotInstalled ? R.string.use_tor_prompt : R.string.use_i2p_prompt)
.setPositiveButton(R.string.yes, dialogClickListener)
.setNegativeButton(R.string.no, dialogClickListener);
}
builder.show();
}
}
/*
* Initialize WebKit Proxying
*/
private void initializeProxy(@NonNull Activity activity) {
String host;
int port;
switch (mPreferences.getProxyChoice()) {
case Constants.NO_PROXY:
// We shouldn't be here
return;
case Constants.PROXY_ORBOT:
if (!OrbotHelper.isOrbotRunning(activity))
OrbotHelper.requestStartTor(activity);
host = "localhost";
port = 8118;
break;
case Constants.PROXY_I2P:
mI2PProxyInitialized = true;
if (mI2PHelperBound && !mI2PHelper.isI2PAndroidRunning()) {
mI2PHelper.requestI2PAndroidStart(activity);
}
host = "localhost";
port = 4444;
break;
default:
host = mPreferences.getProxyHost();
port = mPreferences.getProxyPort();
}
try {
WebkitProxy.setProxy(BrowserApp.class.getName(), activity.getApplicationContext(), null, host, port);
} catch (Exception e) {
Log.d(Constants.TAG, "error enabling web proxying", e);
}
}
public boolean isProxyReady() {
if (mPreferences.getProxyChoice() == Constants.PROXY_I2P) {
if (!mI2PHelper.isI2PAndroidRunning()) {
mBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.i2p_not_running));
return false;
} else if (!mI2PHelper.areTunnelsActive()) {
mBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.i2p_tunnels_not_ready));
return false;
}
}
return true;
}
public void updateProxySettings(@NonNull Activity activity) {
if (mPreferences.getUseProxy()) {
initializeProxy(activity);
} else {
try {
WebkitProxy.resetProxy(BrowserApp.class.getName(), activity.getApplicationContext());
} catch (Exception e) {
e.printStackTrace();
}
mI2PProxyInitialized = false;
}
}
public void onStop() {
mI2PHelper.unbind();
mI2PHelperBound = false;
}
public void onStart(final Activity activity) {
if (mPreferences.getProxyChoice() == Constants.PROXY_I2P) {
// Try to bind to I2P Android
mI2PHelper.bind(new I2PAndroidHelper.Callback() {
@Override
public void onI2PAndroidBound() {
mI2PHelperBound = true;
if (mI2PProxyInitialized && !mI2PHelper.isI2PAndroidRunning())
mI2PHelper.requestI2PAndroidStart(activity);
}
});
}
}
public static int setProxyChoice(int choice, @NonNull Activity activity) {
switch (choice) {
case Constants.PROXY_ORBOT:
if (!OrbotHelper.isOrbotInstalled(activity)) {
choice = Constants.NO_PROXY;
Utils.showSnackbar(activity, R.string.install_orbot);
}
break;
case Constants.PROXY_I2P:
I2PAndroidHelper ih = new I2PAndroidHelper(BrowserApp.get(activity));
if (!ih.isI2PAndroidInstalled()) {
choice = Constants.NO_PROXY;
ih.promptToInstall(activity);
}
break;
case Constants.PROXY_MANUAL:
break;
}
return choice;
}
}
@@ -0,0 +1,105 @@
package acr.browser.lightning.utils;
import android.content.Context;
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.support.annotation.AttrRes;
import android.support.annotation.ColorInt;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.util.TypedValue;
import android.widget.ImageView;
import acr.browser.lightning.R;
public class ThemeUtils {
private static final TypedValue sTypedValue = new TypedValue();
public static int getPrimaryColor(@NonNull Context context) {
return getColor(context, R.attr.colorPrimary);
}
public static int getPrimaryColorDark(@NonNull Context context) {
return getColor(context, R.attr.colorPrimaryDark);
}
public static int getAccentColor(@NonNull Context context) {
return getColor(context, R.attr.colorAccent);
}
private static int getColor(@NonNull Context context, @AttrRes int resource) {
TypedArray a = context.obtainStyledAttributes(sTypedValue.data, new int[]{resource});
int color = a.getColor(0, 0);
a.recycle();
return color;
}
@ColorInt
public static int getIconLightThemeColor(@NonNull Context context) {
return ContextCompat.getColor(context, R.color.icon_light_theme);
}
@ColorInt
public static int getIconDarkThemeColor(@NonNull Context context) {
return ContextCompat.getColor(context, R.color.icon_dark_theme);
}
@ColorInt
public static int getIconThemeColor(@NonNull Context context, boolean dark) {
return (dark) ? getIconDarkThemeColor(context) : getIconLightThemeColor(context);
}
public static void themeImageView(@NonNull ImageView icon, @NonNull Context context, boolean dark) {
int color = dark ? getIconDarkThemeColor(context) : getIconLightThemeColor(context);
icon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
}
@NonNull
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);
Paint p = new Paint();
ColorFilter filter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN);
p.setColorFilter(filter);
Canvas canvas = new Canvas(resultBitmap);
canvas.drawBitmap(sourceBitmap, 0, 0, p);
sourceBitmap.recycle();
return resultBitmap;
}
@NonNull
public static Drawable getThemedDrawable(@NonNull Context context, @DrawableRes int res, boolean dark) {
int color = dark ? getIconDarkThemeColor(context) : getIconLightThemeColor(context);
final Drawable drawable = ContextCompat.getDrawable(context, res);
drawable.mutate();
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
return drawable;
}
@NonNull
public static ColorDrawable getSelectedBackground(@NonNull Context context, boolean dark) {
@ColorInt final int color = (dark) ? ContextCompat.getColor(context, R.color.selected_dark) :
ContextCompat.getColor(context, R.color.selected_light);
return new ColorDrawable(color);
}
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);
}
}
@@ -0,0 +1,199 @@
/*
* Copyright (C) 2010 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.utils;
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 {
private static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile(
"(?i)" + // switch on case insensitive matching
'(' + // begin group for schema
"(?:http|https|file)://" +
"|(?:inline|data|about|javascript):" +
"|(?:.*:.*@)" +
')' +
"(.*)");
// Google search
public final static String QUERY_PLACE_HOLDER = "%s";
// Regular expression to strip http:// and optionally
// the trailing slash
private static final Pattern STRIP_URL_PATTERN =
Pattern.compile("^http://(.*?)/?$");
private UrlUtils() { /* cannot be instantiated */ }
/**
* Strips the provided url of preceding "http://" and any trailing "/". Does not
* strip "https://". If the provided string cannot be stripped, the original string
* is returned.
* <p/>
* TODO: Put this in TextUtils to be used by other packages doing something similar.
*
* @param url a url to strip, like "http://www.google.com/"
* @return a stripped url like "www.google.com", or the original string if it could
* not be stripped
*/
@Nullable
public static String stripUrl(@Nullable String url) {
if (url == null) return null;
Matcher m = STRIP_URL_PATTERN.matcher(url);
if (m.matches()) {
return m.group(1);
} else {
return url;
}
}
/**
* Attempts to determine whether user input is a URL or search
* terms. Anything with a space is passed to search if canBeSearch is true.
* <p/>
* Converts to lowercase any mistakenly uppercased schema (i.e.,
* "Http://" converts to "http://"
*
* @param canBeSearch If true, will return a search url if it isn't a valid
* URL. If false, invalid URLs will return null
* @return Original or modified URL
*/
@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);
if (matcher.matches()) {
// force scheme to lowercase
String scheme = matcher.group(1);
String lcScheme = scheme.toLowerCase();
if (!lcScheme.equals(scheme)) {
inUrl = lcScheme + matcher.group(2);
}
if (hasSpace && Patterns.WEB_URL.matcher(inUrl).matches()) {
inUrl = inUrl.replace(" ", "%20");
}
return inUrl;
}
if (!hasSpace) {
if (Patterns.WEB_URL.matcher(inUrl).matches()) {
return URLUtil.guessUrl(inUrl);
}
}
if (canBeSearch) {
return URLUtil.composeSearchUrl(inUrl,
searchUrl, QUERY_PLACE_HOLDER);
}
return "";
}
/* package */
@NonNull
static String fixUrl(@NonNull String inUrl) {
// FIXME: Converting the url to lower case
// duplicates functionality in smartUrlFilter().
// However, changing all current callers of fixUrl to
// call smartUrlFilter in addition may have unwanted
// consequences, and is deferred for now.
int colon = inUrl.indexOf(':');
boolean allLower = true;
for (int index = 0; index < colon; index++) {
char ch = inUrl.charAt(index);
if (!Character.isLetter(ch)) {
break;
}
allLower &= Character.isLowerCase(ch);
if (index == colon - 1 && !allLower) {
inUrl = inUrl.substring(0, colon).toLowerCase()
+ inUrl.substring(colon);
}
}
if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
return inUrl;
if (inUrl.startsWith("http:") ||
inUrl.startsWith("https:")) {
if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) {
inUrl = inUrl.replaceFirst("/", "//");
} else inUrl = inUrl.replaceFirst(":", "://");
}
return inUrl;
}
// Returns the filtered URL. Cannot return null, but can return an empty string
/* package */
@Nullable
static String filteredUrl(@Nullable String inUrl) {
if (inUrl == null) {
return "";
}
if (inUrl.startsWith("content:")
|| inUrl.startsWith("browser:")) {
return "";
}
return inUrl;
}
/**
* Returns whether the given url is the bookmarks/history page or a normal website
*/
public static boolean isSpecialUrl(@Nullable String url) {
return url != null && url.startsWith(Constants.FILE) &&
(url.endsWith(BookmarkPage.FILENAME) ||
url.endsWith(HistoryPage.FILENAME) ||
url.endsWith(StartPage.FILENAME));
}
/**
* 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);
}
}

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