242 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
Stefano Pacifici ab7273106f Merge branch 'dev' of github.com:anthonycr/Lightning-Browser into experimental_tabs 2015-10-05 17:56:12 +02:00
Stefano Pacifici 61b57cd992 Restore activity restart when tab mode changes 2015-09-29 14:39:05 +02:00
Stefano Pacifici 3cb576d358 Merge latest changes from Anthony's dev branch 2015-09-29 12:11:08 +02: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
Stefano Pacifici 6749ca39b8 Simplified LightningView with externalized XXXClients 2015-09-22 16:15:17 +02: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
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
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
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
133 arquivos alterados com 11947 adições e 4019 exclusões
+2
Ver Arquivo
@@ -50,3 +50,5 @@ out
# Source:
# https://raw.githubusercontent.com/github/gitignore/master/Android.gitignore
# https://gitlab.com/fdroid/fdroidclient/raw/master/.gitignore
*.iml
+3 -1
Ver Arquivo
@@ -2,8 +2,9 @@ language: android
sudo: false
android:
components:
- tools
- build-tools-23.0.3
- build-tools-22.0.1
- build-tools-23.0.1
- android-23
- android-22
- extra-android-support
@@ -17,3 +18,4 @@ before_install:
install:
- ./gradlew
script:
- ./gradlew assembleDebug --stacktrace
-139
Ver Arquivo
@@ -1,139 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="Lightning-Browser" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":app" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="lightningPlusDebug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleLightningPlusDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileLightningPlusDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleLightningPlusDebugAndroidTest" />
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileLightningPlusDebugAndroidTestSources" />
<afterSyncTasks>
<task>generateLightningPlusDebugAndroidTestSources</task>
<task>generateLightningPlusDebugSources</task>
</afterSyncTasks>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/lightningPlus/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/lightningPlus/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/lightningPlus/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/lightningPlus/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/lightningPlus/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/lightningPlus/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/lightningPlus/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/lightningPlus/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/lightningPlus/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/lightningPlusDebug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/lightningPlusDebug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/lightningPlusDebug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/lightningPlusDebug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/lightningPlusDebug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/lightningPlusDebug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/lightningPlusDebug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/lightningPlus/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/lightningPlus/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/lightningPlus/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/lightningPlus/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/lightningPlus/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/lightningPlus/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/LightningPlus/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/LightningPlus/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/LightningPlus/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/LightningPlus/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/LightningPlus/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/LightningPlus/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/LightningPlus/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestLightningPlus/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestLightningPlus/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestLightningPlus/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestLightningPlus/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestLightningPlus/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestLightningPlus/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestLightningPlus/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/appcompat-v7/23.0.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/design/23.0.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/palette-v7/23.0.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/recyclerview-v7/23.0.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/23.0.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.squareup.leakcanary/leakcanary-android/1.3.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/net.i2p.android/client/0.7/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="client-0.7" level="project" />
<orderEntry type="library" exported="" name="leakcanary-analyzer-1.3.1" level="project" />
<orderEntry type="library" exported="" name="jsr250-api-1.0" level="project" />
<orderEntry type="library" exported="" name="butterknife-7.0.1" level="project" />
<orderEntry type="library" exported="" name="dagger-2.0.1" level="project" />
<orderEntry type="library" exported="" name="leakcanary-watcher-1.3.1" level="project" />
<orderEntry type="library" exported="" name="otto-1.3.8" level="project" />
<orderEntry type="library" exported="" name="support-v4-23.0.1" level="project" />
<orderEntry type="library" exported="" name="haha-1.3" level="project" />
<orderEntry type="library" exported="" name="palette-v7-23.0.1" level="project" />
<orderEntry type="library" exported="" name="design-23.0.1" level="project" />
<orderEntry type="library" exported="" name="appcompat-v7-23.0.1" level="project" />
<orderEntry type="library" exported="" name="javax.inject-1" level="project" />
<orderEntry type="library" exported="" name="jsoup-1.8.3" level="project" />
<orderEntry type="library" exported="" name="leakcanary-android-1.3.1" level="project" />
<orderEntry type="library" exported="" name="recyclerview-v7-23.0.1" level="project" />
<orderEntry type="library" exported="" name="support-annotations-23.0.1" level="project" />
<orderEntry type="module" module-name="libnetcipher" exported="" />
</component>
</module>
+57 -19
Ver Arquivo
@@ -1,18 +1,27 @@
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
apply plugin: 'com.getkeepsafe.dexcount'
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 14
targetSdkVersion 23
versionName "4.2.3.1"
versionName "4.3.3"
generatedDensities = []
}
aaptOptions {
additionalParameters "--no-version-vectors"
}
sourceSets {
lightningPlus.setRoot('src/LightningPlus')
lightningLite.setRoot('src/LightningLite')
}
buildTypes {
debug {
minifyEnabled false
@@ -26,44 +35,73 @@ android {
proguardFiles 'proguard-project.txt'
}
}
productFlavors {
lightningPlus {
buildConfigField "boolean", "FULL_VERSION", "true"
applicationId "acr.browser.lightning"
versionCode 85
versionCode 88
}
lightningLite {
buildConfigField "boolean", "FULL_VERSION", "false"
applicationId "acr.browser.barebones"
versionCode 86
versionCode 90
}
}
lintOptions {
abortOnError false
abortOnError true
}
packagingOptions {
exclude '.readme'
}
}
dexcount {
includeClasses = false
includeFieldCount = false
printAsTree = true
orderByMethodCount = true
verbose = false
}
dependencies {
compile 'com.android.support:palette-v7:23.0.1'
compile 'com.android.support:appcompat-v7:23.0.1'
compile 'com.android.support:design:23.0.1'
compile 'com.android.support:recyclerview-v7:23.0.1'
compile 'org.jsoup:jsoup:1.8.3'
// 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'
compile 'com.google.dagger:dagger:2.0.1'
apt 'com.google.dagger:dagger-compiler:2.0.1'
// 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'
// Only Lightning Plus needs the proxy libraries
compile 'net.i2p.android:client:0.7'
// 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'))
compile project(':libnetcipher')
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'
provided 'javax.annotation:jsr250-api:1.0'
// memory leak analysis
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
}
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
@@ -1,214 +0,0 @@
package acr.browser.lightning.utils;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import net.i2p.android.ui.I2PAndroidHelper;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.preference.PreferenceManager;
import info.guardianproject.netcipher.proxy.OrbotHelper;
import info.guardianproject.netcipher.web.WebkitProxy;
/**
* 6/4/2015 Anthony Restaino
*/
public class ProxyUtils {
// Helper
private final I2PAndroidHelper mI2PHelper;
private static boolean mI2PHelperBound;
private static boolean mI2PProxyInitialized;
private final PreferenceManager mPreferences;
private static ProxyUtils mInstance;
private ProxyUtils(Context context) {
mPreferences = PreferenceManager.getInstance();
mI2PHelper = new I2PAndroidHelper(context.getApplicationContext());
}
public static ProxyUtils getInstance() {
if (mInstance == null) {
mInstance = new ProxyUtils(BrowserApp.getAppContext());
}
return mInstance;
}
/*
* If Orbot/Tor or I2P is installed, prompt the user if they want to enable
* proxying for this session
*/
public void checkForProxy(final Activity activity) {
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(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(Activity activity) {
if (mPreferences.getProxyChoice() == Constants.PROXY_I2P) {
if (!mI2PHelper.isI2PAndroidRunning()) {
Utils.showSnackbar(activity, R.string.i2p_not_running);
return false;
} else if (!mI2PHelper.areTunnelsActive()) {
Utils.showSnackbar(activity, R.string.i2p_tunnels_not_ready);
return false;
}
}
return true;
}
public void updateProxySettings(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, 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(activity.getApplicationContext());
if (!ih.isI2PAndroidInstalled()) {
choice = Constants.NO_PROXY;
ih.promptToInstall(activity);
}
break;
case Constants.PROXY_MANUAL:
break;
}
return choice;
}
}
+75 -57
Ver Arquivo
@@ -1,24 +1,26 @@
<?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">
<?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="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.android.browser.permission.READ_HISTORY_BOOKMARKS"/>
<uses-permission android:name="com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"/>
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
android:required="false"/>
<uses-feature
android:name="android.hardware.location"
android:required="false" />
android:required="false"/>
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
android:required="false"/>
<application
android:name=".app.BrowserApp"
@@ -32,65 +34,82 @@
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/Theme.LightTheme">
android:theme="@style/Theme.LightTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.APP_BROWSER" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.APP_BROWSER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="about" />
<data android:scheme="javascript" />
<data android:scheme="file"/>
<data android:mimeType="text/html"/>
<data android:mimeType="text/plain"/>
<data android:mimeType="application/xhtml+xml"/>
<data android:mimeType="application/vnd.wap.xhtml+xml"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="about"/>
<data android:scheme="javascript"/>
</intent-filter>
<!--
For these schemes where any of these particular MIME types
have been supplied, we are a good candidate.
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="inline" />
<data android:mimeType="text/html" />
<data android:mimeType="text/plain" />
<data android:mimeType="application/xhtml+xml" />
<data android:mimeType="application/vnd.wap.xhtml+xml" />
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="inline"/>
<data android:mimeType="text/html"/>
<data android:mimeType="text/plain"/>
<data android:mimeType="application/xhtml+xml"/>
<data android:mimeType="application/vnd.wap.xhtml+xml"/>
</intent-filter>
<!-- For viewing saved web archives. -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="file" />
<data android:mimeType="application/x-webarchive-xml" />
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="file"/>
<data android:mimeType="application/x-webarchive-xml"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.WEB_SEARCH" />
<action android:name="android.intent.action.WEB_SEARCH"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="http"/>
<data android:scheme="https"/>
</intent-filter>
<intent-filter>
<action android:name="info.guardianproject.panic.action.TRIGGER"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
@@ -99,9 +118,9 @@
android:label="@string/settings"
android:theme="@style/Theme.SettingsTheme">
<intent-filter>
<action android:name="android.intent.action.SETTINGS" />
<action android:name="android.intent.action.SETTINGS"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
@@ -110,13 +129,12 @@
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/app_name"
android:launchMode="singleTask"
android:process=":incognito"
android:theme="@style/Theme.DarkTheme"
android:windowSoftInputMode="stateHidden">
android:windowSoftInputMode="stateHidden|adjustResize">
<intent-filter>
<action android:name="android.intent.action.INCOGNITO" />
<action android:name="android.intent.action.INCOGNITO"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
@@ -125,9 +143,9 @@
android:label="@string/reading_mode"
android:theme="@style/Theme.SettingsTheme">
<intent-filter>
<action android:name="android.intent.action.READING" />
<action android:name="android.intent.action.READING"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
</application>
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
@@ -2,28 +2,33 @@ package acr.browser.lightning.activity;
import android.content.Intent;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.Menu;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import acr.browser.lightning.R;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.react.Action;
import acr.browser.lightning.react.Observable;
import acr.browser.lightning.react.Subscriber;
@SuppressWarnings("deprecation")
public class IncognitoActivity extends BrowserActivity {
@Override
public void updateCookiePreference() {
CookieManager cookieManager = CookieManager.getInstance();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(this);
}
cookieManager.setAcceptCookie(PreferenceManager.getInstance().getIncognitoCookiesEnabled());
}
@Override
public synchronized void initializeTabs() {
newTab(null, true);
public Observable<Void> updateCookiePreference() {
return Observable.create(new Action<Void>() {
@Override
public void onSubscribe(@NonNull Subscriber<Void> subscriber) {
CookieManager cookieManager = CookieManager.getInstance();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(IncognitoActivity.this);
}
cookieManager.setAcceptCookie(mPreferences.getIncognitoCookiesEnabled());
subscriber.onComplete();
}
});
}
@Override
@@ -45,7 +50,7 @@ public class IncognitoActivity extends BrowserActivity {
}
@Override
public void updateHistory(String title, String url) {
public void updateHistory(@Nullable String title, @NonNull String url) {
// addItemToHistory(title, url);
}
@@ -56,7 +61,11 @@ public class IncognitoActivity extends BrowserActivity {
@Override
public void closeActivity() {
closeDrawers();
finish();
closeDrawers(new Runnable() {
@Override
public void run() {
closeBrowser();
}
});
}
}
@@ -2,29 +2,33 @@ package acr.browser.lightning.activity;
import android.content.Intent;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.Menu;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import acr.browser.lightning.R;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.react.Action;
import acr.browser.lightning.react.Observable;
import acr.browser.lightning.react.Subscriber;
@SuppressWarnings("deprecation")
public class MainActivity extends BrowserActivity {
@Override
public void updateCookiePreference() {
CookieManager cookieManager = CookieManager.getInstance();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(this);
}
cookieManager.setAcceptCookie(PreferenceManager.getInstance().getCookiesEnabled());
}
@Override
public synchronized void initializeTabs() {
restoreOrNewTab();
// if incognito mode use newTab(null, true); instead
public Observable<Void> updateCookiePreference() {
return Observable.create(new Action<Void>() {
@Override
public void onSubscribe(@NonNull Subscriber<Void> subscriber) {
CookieManager cookieManager = CookieManager.getInstance();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
CookieSyncManager.createInstance(MainActivity.this);
}
cookieManager.setAcceptCookie(mPreferences.getCookiesEnabled());
subscriber.onComplete();
}
});
}
@Override
@@ -35,8 +39,12 @@ public class MainActivity extends BrowserActivity {
@Override
protected void onNewIntent(Intent intent) {
handleNewIntent(intent);
super.onNewIntent(intent);
if (isPanicTrigger(intent)) {
panicClean();
} else {
handleNewIntent(intent);
super.onNewIntent(intent);
}
}
@Override
@@ -46,7 +54,7 @@ public class MainActivity extends BrowserActivity {
}
@Override
public void updateHistory(String title, String url) {
public void updateHistory(@Nullable String title, @NonNull String url) {
addItemToHistory(title, url);
}
@@ -57,8 +65,13 @@ public class MainActivity extends BrowserActivity {
@Override
public void closeActivity() {
closeDrawers();
moveTaskToBack(true);
closeDrawers(new Runnable() {
@Override
public void run() {
performExitCleanUp();
moveTaskToBack(true);
}
});
}
@@ -1,18 +1,19 @@
package acr.browser.lightning.activity;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.graphics.drawable.ColorDrawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@@ -21,23 +22,42 @@ 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 TextView mTitle;
private TextView mBody;
private static final String TAG = ReadingActivity.class.getSimpleName();
@Bind(R.id.textViewTitle)
TextView mTitle;
@Bind(R.id.textViewBody)
TextView mBody;
@Inject PreferenceManager mPreferences;
private boolean mInvert;
private String mUrl = null;
private PreferenceManager mPreferences;
private int mTextSize;
private ProgressDialog mProgressDialog;
private Subscription mPageLoaderSubscription;
private static final float XXLARGE = 30.0f;
private static final float XLARGE = 26.0f;
@@ -48,8 +68,9 @@ public class ReadingActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
BrowserApp.getAppComponent().inject(this);
overridePendingTransition(R.anim.slide_in_from_right, R.anim.fade_out_scale);
mPreferences = PreferenceManager.getInstance();
mInvert = mPreferences.getInvertColors();
final int color;
if (mInvert) {
@@ -63,6 +84,7 @@ public class ReadingActivity extends AppCompatActivity {
}
super.onCreate(savedInstanceState);
setContentView(R.layout.reading_view);
ButterKnife.bind(this);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
@@ -70,9 +92,6 @@ public class ReadingActivity extends AppCompatActivity {
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mTitle = (TextView) findViewById(R.id.textViewTitle);
mBody = (TextView) findViewById(R.id.textViewBody);
mTextSize = mPreferences.getReadingTextSize();
mBody.setTextSize(getTextSize(mTextSize));
mTitle.setText(getString(R.string.untitled));
@@ -129,66 +148,87 @@ public class ReadingActivity extends AppCompatActivity {
}
if (getSupportActionBar() != null)
getSupportActionBar().setTitle(Utils.getDomainName(mUrl));
new PageLoader(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mUrl);
mPageLoaderSubscription = loadPage(mUrl).subscribeOn(Schedulers.worker())
.observeOn(Schedulers.main())
.subscribe(new OnSubscribe<ReaderInfo>() {
@Override
public void onStart() {
mProgressDialog = new ProgressDialog(ReadingActivity.this);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
mProgressDialog.setCancelable(false);
mProgressDialog.setIndeterminate(true);
mProgressDialog.setMessage(getString(R.string.loading));
mProgressDialog.show();
}
@Override
public void onNext(@Nullable ReaderInfo item) {
if (item == null || item.getTitle().isEmpty() || item.getBody().isEmpty()) {
setText(getString(R.string.untitled), getString(R.string.loading_failed));
} else {
setText(item.getTitle(), item.getBody());
}
}
@Override
public void onError(@NonNull Throwable throwable) {
setText(getString(R.string.untitled), getString(R.string.loading_failed));
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
mProgressDialog = null;
}
}
@Override
public void onComplete() {
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
mProgressDialog = null;
}
}
});
return true;
}
private class PageLoader extends AsyncTask<String, Void, Void> {
private final Activity mActivity;
private String mTitleText;
private String mBodyText;
public PageLoader(Activity activity) {
mActivity = activity;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
mProgressDialog = new ProgressDialog(mActivity);
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
mProgressDialog.setCancelable(false);
mProgressDialog.setIndeterminate(true);
mProgressDialog.setMessage(mActivity.getString(R.string.loading));
mProgressDialog.show();
}
@Override
protected Void doInBackground(String... params) {
HtmlFetcher fetcher = new HtmlFetcher();
try {
JResult result = fetcher.fetchAndExtract(params[0], 2500, true);
mTitleText = result.getTitle();
mBodyText = result.getText();
} catch (Exception e) {
mTitleText = "";
mBodyText = "";
e.printStackTrace();
} catch (OutOfMemoryError e) {
System.gc();
mTitleText = "";
mBodyText = "";
e.printStackTrace();
private static Observable<ReaderInfo> loadPage(@NonNull final String url) {
return Observable.create(new Action<ReaderInfo>() {
@Override
public void onSubscribe(@NonNull Subscriber<ReaderInfo> subscriber) {
HtmlFetcher fetcher = new HtmlFetcher();
try {
JResult result = fetcher.fetchAndExtract(url, 2500, true);
subscriber.onNext(new ReaderInfo(result.getTitle(), result.getText()));
} catch (Exception e) {
subscriber.onError(new Throwable("Encountered exception"));
Log.e(TAG, "Error parsing page", e);
} catch (OutOfMemoryError e) {
System.gc();
subscriber.onError(new Throwable("Out of memory"));
Log.e(TAG, "Out of memory", e);
}
subscriber.onComplete();
}
return null;
});
}
private static class ReaderInfo {
@NonNull private final String mTitleText;
@NonNull private final String mBodyText;
public ReaderInfo(@NonNull String title, @NonNull String body) {
mTitleText = title;
mBodyText = body;
}
@Override
protected void onPostExecute(Void result) {
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
mProgressDialog = null;
}
if (mTitleText.isEmpty() || mBodyText.isEmpty()) {
setText(getString(R.string.untitled), getString(R.string.loading_failed));
} else {
setText(mTitleText, mBodyText);
}
super.onPostExecute(result);
@NonNull
public String getTitle() {
return mTitleText;
}
@NonNull
public String getBody() {
return mBodyText;
}
}
private void setText(String title, String body) {
@@ -219,6 +259,8 @@ public class ReadingActivity extends AppCompatActivity {
@Override
protected void onDestroy() {
mPageLoaderSubscription.unsubscribe();
if (mProgressDialog != null && mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
mProgressDialog = null;
@@ -3,6 +3,7 @@
*/
package acr.browser.lightning.activity;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.widget.Toolbar;
@@ -11,15 +12,18 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import com.anthonycr.grant.PermissionsManager;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import acr.browser.lightning.R;
import acr.browser.lightning.utils.PermissionsManager;
import acr.browser.lightning.app.BrowserApp;
public class SettingsActivity extends ThemableSettingsActivity {
private static final List<String> fragments = new ArrayList<>();
private static final List<String> mFragments = new ArrayList<>(7);
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -42,15 +46,30 @@ public class SettingsActivity extends ThemableSettingsActivity {
@Override
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.preferences_headers, target);
fragments.clear();
for (Header header : target) {
fragments.add(header.fragment);
mFragments.clear();
Iterator<Header> headerIterator = target.iterator();
while (headerIterator.hasNext()) {
Header header = headerIterator.next();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// Workaround for bug in the AppCompat support library
header.iconRes = R.drawable.empty;
}
if (header.titleRes == R.string.debug_title) {
if (BrowserApp.isRelease()) {
headerIterator.remove();
} else {
mFragments.add(header.fragment);
}
} else {
mFragments.add(header.fragment);
}
}
}
@Override
protected boolean isValidFragment(String fragmentName) {
return fragments.contains(fragmentName);
return mFragments.contains(fragmentName);
}
@Override
@@ -61,7 +80,7 @@ public class SettingsActivity extends ThemableSettingsActivity {
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
PermissionsManager.getInstance().notifyPermissionsChange(permissions);
PermissionsManager.getInstance().notifyPermissionsChange(permissions, grantResults);
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
@@ -0,0 +1,533 @@
package acr.browser.lightning.activity;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.webkit.WebView;
import com.squareup.otto.Bus;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.BookmarkPage;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.constant.HistoryPage;
import acr.browser.lightning.constant.StartPage;
import acr.browser.lightning.database.BookmarkManager;
import acr.browser.lightning.database.HistoryDatabase;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.react.Action;
import acr.browser.lightning.react.Observable;
import acr.browser.lightning.react.OnSubscribe;
import acr.browser.lightning.react.Schedulers;
import acr.browser.lightning.react.Subscriber;
import acr.browser.lightning.utils.FileUtils;
import acr.browser.lightning.utils.UrlUtils;
import acr.browser.lightning.view.LightningView;
/**
* A manager singleton that holds all the {@link LightningView}
* and tracks the current tab. It handles creation, deletion,
* restoration, state saving, and switching of tabs.
*/
public class TabsManager {
private static final String TAG = TabsManager.class.getSimpleName();
private static final String BUNDLE_KEY = "WEBVIEW_";
private static final String URL_KEY = "URL_KEY";
private static final String BUNDLE_STORAGE = "SAVED_TABS.parcel";
private final List<LightningView> mTabList = new ArrayList<>(1);
@Nullable private LightningView mCurrentTab;
@Nullable private TabNumberChangedListener mTabNumberListener;
private boolean mIsInitialized = false;
private final List<Runnable> mPostInitializationWorkList = new ArrayList<>();
@Inject PreferenceManager mPreferenceManager;
@Inject BookmarkManager mBookmarkManager;
@Inject HistoryDatabase mHistoryManager;
@Inject Bus mEventBus;
@Inject Application mApp;
public TabsManager() {
BrowserApp.getAppComponent().inject(this);
}
// TODO remove and make presenter call new tab methods so it always knows
@Deprecated
public interface TabNumberChangedListener {
void tabNumberChanged(int newNumber);
}
public void setTabNumberChangedListener(@Nullable TabNumberChangedListener listener) {
mTabNumberListener = listener;
}
public void cancelPendingWork() {
mPostInitializationWorkList.clear();
}
public void doAfterInitialization(@NonNull Runnable runnable) {
if (mIsInitialized) {
runnable.run();
} else {
mPostInitializationWorkList.add(runnable);
}
}
private void finishInitialization() {
mIsInitialized = true;
for (Runnable runnable : mPostInitializationWorkList) {
runnable.run();
}
}
/**
* Restores old tabs that were open before the browser
* was closed. Handles the intent used to open the browser.
*
* @param activity the activity needed to create tabs.
* @param intent the intent that started the browser activity.
* @param incognito whether or not we are in incognito mode.
*/
public synchronized Observable<Void> initializeTabs(@NonNull final Activity activity,
@Nullable final Intent intent,
final boolean incognito) {
return Observable.create(new Action<Void>() {
@Override
public void onSubscribe(@NonNull final Subscriber<Void> subscriber) {
// Make sure we start with a clean tab list
shutdown();
// If incognito, only create one tab, do not handle intent
// in order to protect user privacy
if (incognito) {
newTab(activity, null, true);
subscriber.onComplete();
return;
}
String url = null;
if (intent != null) {
url = intent.getDataString();
}
Log.d(TAG, "URL from intent: " + url);
mCurrentTab = null;
if (mPreferenceManager.getRestoreLostTabsEnabled()) {
restoreLostTabs(url, activity, subscriber);
} else {
newTab(activity, null, false);
finishInitialization();
subscriber.onComplete();
}
}
});
}
private void restoreLostTabs(@Nullable final String url, @NonNull final Activity activity,
@NonNull final Subscriber subscriber) {
restoreState().subscribeOn(Schedulers.io())
.observeOn(Schedulers.main()).subscribe(new OnSubscribe<Bundle>() {
@Override
public void onNext(Bundle item) {
LightningView tab = newTab(activity, "", false);
String url = item.getString(URL_KEY);
if (url != null && tab.getWebView() != null) {
if (UrlUtils.isBookmarkUrl(url)) {
new BookmarkPage(tab, activity, mBookmarkManager).load();
} else if (UrlUtils.isStartPageUrl(url)) {
new StartPage(tab, mApp).load();
} else if (UrlUtils.isHistoryUrl(url)) {
new HistoryPage(tab, mApp, mHistoryManager).load();
}
} else if (tab.getWebView() != null) {
tab.getWebView().restoreState(item);
}
}
@Override
public void onComplete() {
if (url != null) {
if (url.startsWith(Constants.FILE)) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setCancelable(true)
.setTitle(R.string.title_warning)
.setMessage(R.string.message_blocked_local)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.action_open, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
newTab(activity, url, false);
}
}).show();
} else {
newTab(activity, url, false);
}
}
if (mTabList.size() == 0) {
newTab(activity, null, false);
}
finishInitialization();
subscriber.onComplete();
}
});
}
/**
* Method used to resume all the tabs in the browser.
* This is necessary because we cannot pause the
* WebView when the app is open currently due to a
* bug in the WebView, where calling onResume doesn't
* consistently resume it.
*
* @param context the context needed to initialize
* the LightningView preferences.
*/
public void resumeAll(@NonNull Context context) {
LightningView current = getCurrentTab();
if (current != null) {
current.resumeTimers();
}
for (LightningView tab : mTabList) {
if (tab != null) {
tab.onResume();
tab.initializePreferences(context);
}
}
}
/**
* Method used to pause all the tabs in the browser.
* This is necessary because we cannot pause the
* WebView when the app is open currently due to a
* bug in the WebView, where calling onResume doesn't
* consistently resume it.
*/
public void pauseAll() {
LightningView current = getCurrentTab();
if (current != null) {
current.pauseTimers();
}
for (LightningView tab : mTabList) {
if (tab != null) {
tab.onPause();
}
}
}
/**
* Return the tab at the given position in tabs list, or
* null if position is not in tabs list range.
*
* @param position the index in tabs list
* @return the corespondent {@link LightningView},
* or null if the index is invalid
*/
@Nullable
public synchronized LightningView getTabAtPosition(final int position) {
if (position < 0 || position >= mTabList.size()) {
return null;
}
return mTabList.get(position);
}
/**
* Frees memory for each tab in the
* manager. Note: this will only work
* on API < KITKAT as on KITKAT onward
* the WebViews manage their own
* memory correctly.
*/
public synchronized void freeMemory() {
for (LightningView tab : mTabList) {
//noinspection deprecation
tab.freeMemory();
}
}
/**
* Shutdown the manager. This destroys
* all tabs and clears the references
* to those tabs. Current tab is also
* released for garbage collection.
*/
public synchronized void shutdown() {
for (LightningView tab : mTabList) {
tab.onDestroy();
}
mTabList.clear();
mIsInitialized = false;
mCurrentTab = null;
}
/**
* Forwards network connection status to the WebViews.
*
* @param isConnected whether there is a network
* connection or not.
*/
public synchronized void notifyConnectionStatus(final boolean isConnected) {
for (LightningView tab : mTabList) {
final WebView webView = tab.getWebView();
if (webView != null) {
webView.setNetworkAvailable(isConnected);
}
}
}
/**
* The current number of tabs in the manager.
*
* @return the number of tabs in the list.
*/
public synchronized int size() {
return mTabList.size();
}
/**
* The index of the last tab in the manager.
*
* @return the last tab in the list or -1 if there are no tabs.
*/
public synchronized int last() {
return mTabList.size() - 1;
}
/**
* The last tab in the tab manager.
*
* @return the last tab, or null if
* there are no tabs.
*/
@Nullable
public synchronized LightningView lastTab() {
if (last() < 0) {
return null;
}
return mTabList.get(last());
}
/**
* Create and return a new tab. The tab is
* automatically added to the tabs list.
*
* @param activity the activity needed to create the tab.
* @param url the URL to initialize the tab with.
* @param isIncognito whether the tab is an incognito
* tab or not.
* @return a valid initialized tab.
*/
@NonNull
public synchronized LightningView newTab(@NonNull final Activity activity,
@Nullable final String url,
final boolean isIncognito) {
Log.d(TAG, "New tab");
final LightningView tab = new LightningView(activity, url, isIncognito);
mTabList.add(tab);
if (mTabNumberListener != null) {
mTabNumberListener.tabNumberChanged(size());
}
return tab;
}
/**
* Removes a tab from the list and destroys the tab.
* If the tab removed is the current tab, the reference
* to the current tab will be nullified.
*
* @param position The position of the tab to remove.
*/
private synchronized void removeTab(final int position) {
if (position >= mTabList.size()) {
return;
}
final LightningView tab = mTabList.remove(position);
if (mCurrentTab == tab) {
mCurrentTab = null;
}
tab.onDestroy();
}
/**
* Deletes a tab from the manager. If the tab
* being deleted is the current tab, this method
* will switch the current tab to a new valid tab.
*
* @param position the position of the tab to delete.
* @return returns true if the current tab
* was deleted, false otherwise.
*/
public synchronized boolean deleteTab(int position) {
Log.d(TAG, "Delete tab: " + position);
final LightningView currentTab = getCurrentTab();
int current = positionOf(currentTab);
if (current == position) {
if (size() == 1) {
mCurrentTab = null;
} else if (current < size() - 1) {
// There is another tab after this one
switchToTab(current + 1);
} else {
switchToTab(current - 1);
}
}
removeTab(position);
if (mTabNumberListener != null) {
mTabNumberListener.tabNumberChanged(size());
}
return current == position;
}
/**
* Return the position of the given tab.
*
* @param tab the tab to look for.
* @return the position of the tab or -1
* if the tab is not in the list.
*/
public synchronized int positionOf(final LightningView tab) {
return mTabList.indexOf(tab);
}
/**
* Saves the state of the current WebViews,
* to a bundle which is then stored in persistent
* storage and can be unparceled.
*/
public void saveState() {
Bundle outState = new Bundle(ClassLoader.getSystemClassLoader());
Log.d(Constants.TAG, "Saving tab state");
for (int n = 0; n < mTabList.size(); n++) {
LightningView tab = mTabList.get(n);
Bundle state = new Bundle(ClassLoader.getSystemClassLoader());
if (tab.getWebView() != null && !UrlUtils.isSpecialUrl(tab.getUrl())) {
tab.getWebView().saveState(state);
outState.putBundle(BUNDLE_KEY + n, state);
} else if (tab.getWebView() != null) {
state.putString(URL_KEY, tab.getUrl());
outState.putBundle(BUNDLE_KEY + n, state);
}
}
FileUtils.writeBundleToStorage(mApp, outState, BUNDLE_STORAGE);
}
/**
* Use this method to clear the saved
* state if you do not wish it to be
* restored when the browser next starts.
*/
public void clearSavedState() {
FileUtils.deleteBundleInStorage(mApp, BUNDLE_STORAGE);
}
/**
* Restores the previously saved tabs from the
* bundle stored in peristent file storage.
* It will create new tabs for each tab saved
* and will delete the saved instance file when
* restoration is complete.
*/
private Observable<Bundle> restoreState() {
return Observable.create(new Action<Bundle>() {
@Override
public void onSubscribe(@NonNull Subscriber<Bundle> subscriber) {
Bundle savedState = FileUtils.readBundleFromStorage(mApp, BUNDLE_STORAGE);
if (savedState != null) {
Log.d(Constants.TAG, "Restoring previous WebView state now");
for (String key : savedState.keySet()) {
if (key.startsWith(BUNDLE_KEY)) {
subscriber.onNext(savedState.getBundle(key));
}
}
}
FileUtils.deleteBundleInStorage(mApp, BUNDLE_STORAGE);
subscriber.onComplete();
}
});
}
/**
* Return the {@link WebView} associated to the current tab,
* or null if there is no current tab.
*
* @return a {@link WebView} or null if there is no current tab.
*/
@Nullable
public synchronized WebView getCurrentWebView() {
return mCurrentTab != null ? mCurrentTab.getWebView() : null;
}
/**
* Returns the index of the current tab.
*
* @return Return the index of the current tab, or -1 if the
* current tab is null.
*/
public int indexOfCurrentTab() {
return mTabList.indexOf(mCurrentTab);
}
/**
* Returns the index of the tab.
*
* @return Return the index of the tab, or -1 if the tab isn't in the list.
*/
public int indexOfTab(LightningView tab) {
return mTabList.indexOf(tab);
}
/**
* Return the current {@link LightningView} or null if
* no current tab has been set.
*
* @return a {@link LightningView} or null if there
* is no current tab.
*/
@Nullable
public synchronized LightningView getCurrentTab() {
return mCurrentTab;
}
/**
* Switch the current tab to the one at the given position.
* It returns the selected tab that has been switced to.
*
* @return the selected tab or null if position is out of tabs range.
*/
@Nullable
public synchronized LightningView switchToTab(final int position) {
Log.d(TAG, "switch to tab: " + position);
if (position < 0 || position >= mTabList.size()) {
Log.e(TAG, "Returning a null LightningView requested for position: " + position);
return null;
} else {
final LightningView tab = mTabList.get(position);
if (tab != null) {
mCurrentTab = tab;
}
return tab;
}
}
}
@@ -3,20 +3,31 @@ package acr.browser.lightning.activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import java.util.ArrayDeque;
import java.util.Queue;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.preference.PreferenceManager;
public abstract class ThemableBrowserActivity extends AppCompatActivity {
@Inject PreferenceManager mPreferences;
private int mTheme;
private boolean mShowTabsInDrawer;
private boolean mShouldRunOnResumeActions = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
mTheme = PreferenceManager.getInstance().getUseTheme();
mShowTabsInDrawer = PreferenceManager.getInstance().getShowTabsInDrawer(!isTablet());
BrowserApp.getAppComponent().inject(this);
mTheme = mPreferences.getUseTheme();
mShowTabsInDrawer = mPreferences.getShowTabsInDrawer(!isTablet());
// set the theme
if (mTheme == 1) {
@@ -27,11 +38,31 @@ public abstract class ThemableBrowserActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus && mShouldRunOnResumeActions) {
mShouldRunOnResumeActions = false;
onWindowVisibleToUserAfterResume();
}
}
/**
* Called after the activity is resumed
* and the UI becomes visible to the user.
* Called by onWindowFocusChanged only if
* onResume has been called.
*/
public void onWindowVisibleToUserAfterResume() {
}
@Override
protected void onResume() {
super.onResume();
int theme = PreferenceManager.getInstance().getUseTheme();
boolean drawerTabs = PreferenceManager.getInstance().getShowTabsInDrawer(!isTablet());
mShouldRunOnResumeActions = true;
int theme = mPreferences.getUseTheme();
boolean drawerTabs = mPreferences.getShowTabsInDrawer(!isTablet());
if (theme != mTheme || mShowTabsInDrawer != drawerTabs) {
restart();
}
@@ -42,8 +73,7 @@ public abstract class ThemableBrowserActivity extends AppCompatActivity {
}
private void restart() {
Intent intent = getIntent();
finish();
startActivity(intent);
startActivity(new Intent(this, getClass()));
}
}
@@ -3,7 +3,10 @@ package acr.browser.lightning.activity;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.ThemeUtils;
@@ -11,9 +14,12 @@ public abstract class ThemableSettingsActivity extends AppCompatPreferenceActivi
private int mTheme;
@Inject PreferenceManager mPreferenceManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
mTheme = PreferenceManager.getInstance().getUseTheme();
BrowserApp.getAppComponent().inject(this);
mTheme = mPreferenceManager.getUseTheme();
// set the theme
if (mTheme == 0) {
@@ -32,7 +38,7 @@ public abstract class ThemableSettingsActivity extends AppCompatPreferenceActivi
@Override
protected void onResume() {
super.onResume();
if (PreferenceManager.getInstance().getUseTheme() != mTheme) {
if (mPreferenceManager.getUseTheme() != mTheme) {
restart();
}
}
@@ -3,16 +3,27 @@ package acr.browser.lightning.app;
import javax.inject.Singleton;
import acr.browser.lightning.activity.BrowserActivity;
import acr.browser.lightning.constant.BookmarkPage;
import acr.browser.lightning.dialog.BookmarksDialogBuilder;
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.object.SearchAdapter;
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;
/**
* Created by Stefano Pacifici on 01/09/15.
*/
@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
@@ -23,9 +34,40 @@ public interface AppComponent {
void inject(BookmarkSettingsFragment fragment);
void inject(SearchAdapter adapter);
void inject(SuggestionsAdapter adapter);
void inject(BookmarksDialogBuilder builder);
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);
void inject(BookmarkPage bookmarkPage);
}
@@ -1,41 +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 acr.browser.lightning.database.BookmarkManager;
import dagger.Module;
import dagger.Provides;
/**
* Created by Stefano Pacifici on 01/09/15.
*/
@Module
public class AppModule {
private final BrowserApp app;
private final Bus bus;
private final BrowserApp mApp;
@NonNull private final Bus mBus;
public AppModule(BrowserApp app) {
this.app = app;
this.bus = new Bus();
this.mApp = app;
this.mBus = new Bus();
}
@Provides
public Application provideApplication() {
return mApp;
}
@Provides
public Context provideContext() {
return app.getApplicationContext();
}
@Provides
@Singleton
public BookmarkManager provideBookmarkManager() {
return new BookmarkManager(app.getApplicationContext());
return mApp.getApplicationContext();
}
@NonNull
@Provides
public Bus provideBus() {
return bus;
return mBus;
}
@NonNull
@Provides
@Singleton
public I2PAndroidHelper provideI2PAndroidHelper() {
return new I2PAndroidHelper(mApp.getApplicationContext());
}
}
@@ -1,33 +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 Context context;
private static AppComponent appComponent;
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();
context = getApplicationContext();
LeakCanary.install(this);
buildDepencyGraph();
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);
}
});
}
public static Context getAppContext() {
return context;
@NonNull
public static BrowserApp get(@NonNull Context context) {
return (BrowserApp) context.getApplicationContext();
}
public static AppComponent getAppComponent() {
return appComponent;
return mAppComponent;
}
private void buildDepencyGraph() {
appComponent = DaggerAppComponent.builder().appModule(new AppModule(this)).build();
@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");
}
}
@@ -22,6 +22,7 @@ public class AsyncExecutor implements Executor {
private AsyncExecutor() {}
@NonNull
public static AsyncExecutor getInstance() {
return INSTANCE;
}
@@ -1,10 +1,13 @@
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;
@@ -15,7 +18,6 @@ import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.URL;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.database.HistoryItem;
import acr.browser.lightning.utils.Utils;
@@ -23,22 +25,27 @@ import acr.browser.lightning.utils.Utils;
public class ImageDownloadTask extends AsyncTask<Void, Void, Bitmap> {
private static final String TAG = ImageDownloadTask.class.getSimpleName();
private static final File mCacheDir = BrowserApp.getAppContext().getCacheDir();
private final WeakReference<ImageView> bmImage;
private final HistoryItem mWeb;
@NonNull private final WeakReference<ImageView> mFaviconImage;
@NonNull private final WeakReference<Context> mContextReference;
@NonNull private final HistoryItem mWeb;
private final String mUrl;
private final Bitmap mDefaultBitmap;
@NonNull private final Bitmap mDefaultBitmap;
public ImageDownloadTask(@NonNull ImageView bmImage, @NonNull HistoryItem web, @NonNull Bitmap defaultBitmap) {
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.bmImage = new WeakReference<>(bmImage);
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;
@@ -46,12 +53,17 @@ public class ImageDownloadTask extends AsyncTask<Void, Void, Bitmap> {
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(mCacheDir, hash + ".png");
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()) {
@@ -130,9 +142,19 @@ public class ImageDownloadTask extends AsyncTask<Void, Void, Bitmap> {
super.onPostExecute(bitmap);
AsyncExecutor.getInstance().notifyThreadFinish();
final Bitmap fav = Utils.padFavicon(bitmap);
ImageView view = bmImage.get();
final ImageView view = mFaviconImage.get();
if (view != null && view.getTag().equals(mWeb.getUrl().hashCode())) {
view.setImageBitmap(fav);
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);
}
@@ -2,37 +2,12 @@ package acr.browser.lightning.bus;
import acr.browser.lightning.database.HistoryItem;
/**
* Created by Stefano Pacifici on 26/08/15.
*/
public final class BookmarkEvents {
private BookmarkEvents() {
// No instances
}
/**
* A bookmark was clicked
*/
public final static class Clicked {
public final HistoryItem bookmark;
public Clicked(final HistoryItem bookmark) {
this.bookmark = bookmark;
}
}
/**
* The user ask to open the bookmark as new tab
*/
public final static class AsNewTab {
public final HistoryItem bookmark;
public AsNewTab(final HistoryItem bookmark) {
this.bookmark = bookmark;
}
}
/**
* The user ask to delete the selected bookmark
*/
@@ -45,29 +20,11 @@ public final class BookmarkEvents {
}
/**
* The user ask to bookmark the currently displayed page
* The user ask to add/del a bookmark to the currently displayed page
*/
public static class WantToBookmarkCurrentPage {
public static class ToggleBookmarkForCurrentPage {
}
/**
* The bookmark was added
*/
public static class Added {
public final HistoryItem item;
public Added(final HistoryItem item) {
this.item = item;
}
}
/**
* The {@link acr.browser.lightning.fragment.BookmarksFragment} want to know the url (and title)
* of the currently shown web page.
*/
// public static class WantInfoAboutCurrentPage {
// }
/**
* 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}
@@ -1,8 +1,8 @@
package acr.browser.lightning.bus;
/**
* Created by Stefano Pacifici on 26/08/15.
*/
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
public final class BrowserEvents {
private BrowserEvents() {
@@ -10,23 +10,21 @@ public final class BrowserEvents {
}
/**
* Used to reply to the {@link acr.browser.lightning.fragment.BookmarksFragment} message
* {@link acr.browser.lightning.bus.BookmarkEvents.WantToBookmarkCurrentPage}. The interaction
* result is a new bookmark added.
* 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 AddBookmark {
public static class BookmarkAdded {
public final String title, url;
public AddBookmark(final String title, final String url) {
public BookmarkAdded(final String title, final String url) {
this.title = title;
this.url = url;
}
}
/**
* Used to reply to {@link acr.browser.lightning.fragment.BookmarksFragment} message
* {@link acr.browser.lightning.bus.BookmarkEvents.WantInfoAboutCurrentPage}. This is generally
* used to update the {@link acr.browser.lightning.fragment.BookmarksFragment} interface.
* 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;
@@ -41,4 +39,52 @@ public final class BrowserEvents {
*/
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 {
}
}
@@ -3,31 +3,44 @@
*/
package acr.browser.lightning.constant;
import android.content.Context;
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 javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.database.BookmarkManager;
import acr.browser.lightning.database.HistoryItem;
import acr.browser.lightning.utils.ThemeUtils;
import acr.browser.lightning.utils.Utils;
import acr.browser.lightning.view.LightningView;
public final class BookmarkPage {
public final class BookmarkPage extends AsyncTask<Void, Void, Void> {
private static final String HEADING = "<!DOCTYPE html><html xmlns=http://www.w3.org/1999/xhtml>\n" +
/**
* 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'>\n" +
"<title>" +
BrowserApp.getAppContext().getString(R.string.action_bookmarks) +
"</title>\n" +
"<meta name=viewport content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'>\n" +
"<title>";
private static final String HEADING_2 = "</title>\n" +
"</head>\n" +
"<style>body{background:#e1e1e1;max-width:100%;min-height:100%}#content{width:100%;max-width:800px;margin:0 auto;text-align:center}.box{vertical-align:middle;text-align:center;position:relative;display:inline-block;height:45px;width:150px;margin:10px;background-color:#fff;box-shadow:0 3px 6px rgba(0,0,0,0.25);font-family:Arial;color:#444;font-size:12px;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px}.box-content{height:25px;width:100%;vertical-align:middle;text-align:center;display:table-cell}p.ellipses{" +
"width:130px;font-size: small;font-family: Arial, Helvetica, 'sans-serif';white-space:nowrap;overflow:hidden;text-align:left;vertical-align:middle;margin:auto;text-overflow:ellipsis;-o-text-overflow:ellipsis;-ms-text-overflow:ellipsis}.box a{width:100%;height:100%;position:absolute;left:0;top:0}img{vertical-align:middle;margin-right:10px;width:20px;height:20px;}.margin{margin:10px}</style>\n" +
@@ -41,7 +54,7 @@ public final class BookmarkPage {
"<p class=ellipses>\n" +
"<img src='";
private static final String PART3 = "http://www.google.com/s2/favicons?domain=";
private static final String PART3 = "https://www.google.com/s2/favicons?domain=";
private static final String PART4 = "' />";
@@ -49,55 +62,90 @@ public final class BookmarkPage {
private static final String END = "</div></body></html>";
@Inject
BookmarkManager manager;
private File mFilesDir;
private File mCacheDir;
private final File FILES_DIR;
private final File CACHE_DIR;
private final Application mApp;
private final BookmarkManager mManager;
@NonNull private final WeakReference<LightningView> mTabReference;
private final Bitmap mFolderIcon;
@NonNull private final String mTitle;
@Inject
public BookmarkPage(Context context) {
BrowserApp.getAppComponent().inject(this);
FILES_DIR = context.getFilesDir();
CACHE_DIR = context.getCacheDir();
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;
}
public void buildBookmarkPage(final String folder, final List<HistoryItem> list) {
@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(FILES_DIR, Constants.BOOKMARKS_FILENAME);
bookmarkWebPage = new File(mFilesDir, FILENAME);
} else {
bookmarkWebPage = new File(FILES_DIR, folder + '-' + Constants.BOOKMARKS_FILENAME);
bookmarkWebPage = new File(mFilesDir, folder + '-' + FILENAME);
}
final StringBuilder bookmarkBuilder = new StringBuilder(BookmarkPage.HEADING);
final StringBuilder bookmarkBuilder = new StringBuilder(HEADING_1 + mTitle + HEADING_2);
final String folderIconPath = Constants.FILE + CACHE_DIR + "/folder.png";
final String folderIconPath = Constants.FILE + mCacheDir + "/folder.png";
for (int n = 0, size = list.size(); n < size; n++) {
final HistoryItem item = list.get(n);
bookmarkBuilder.append(BookmarkPage.PART1);
bookmarkBuilder.append(PART1);
if (item.isFolder()) {
final File folderPage = new File(FILES_DIR, item.getTitle() + '-' + Constants.BOOKMARKS_FILENAME);
final File folderPage = new File(mFilesDir, item.getTitle() + '-' + FILENAME);
bookmarkBuilder.append(Constants.FILE).append(folderPage);
bookmarkBuilder.append(BookmarkPage.PART2);
bookmarkBuilder.append(PART2);
bookmarkBuilder.append(folderIconPath);
new Thread(new Runnable() {
@Override
public void run() {
buildBookmarkPage(item.getTitle(), manager.getBookmarksFromFolder(item.getTitle(), true));
}
}).run();
buildBookmarkPage(item.getTitle(), manager);
} else {
bookmarkBuilder.append(item.getUrl());
bookmarkBuilder.append(BookmarkPage.PART2).append(BookmarkPage.PART3);
bookmarkBuilder.append(PART2).append(PART3);
bookmarkBuilder.append(item.getUrl());
}
bookmarkBuilder.append(BookmarkPage.PART4);
bookmarkBuilder.append(PART4);
bookmarkBuilder.append(item.getTitle());
bookmarkBuilder.append(BookmarkPage.PART5);
bookmarkBuilder.append(PART5);
}
bookmarkBuilder.append(BookmarkPage.END);
bookmarkBuilder.append(END);
FileWriter bookWriter = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
bookWriter = new FileWriter(bookmarkWebPage, false);
bookWriter.write(bookmarkBuilder.toString());
} catch (IOException e) {
@@ -107,4 +155,8 @@ public final class BookmarkPage {
}
}
public void load() {
executeOnExecutor(BrowserApp.getIOThread());
}
}
@@ -27,6 +27,22 @@ public final class Constants {
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";
@@ -43,12 +59,9 @@ public final class Constants {
public static final int PROXY_I2P = 2;
public static final int PROXY_MANUAL = 3;
/**
* The bookmark page standard suffix
*/
public static final String BOOKMARKS_FILENAME = "bookmarks.html";
public static final String DEFAULT_ENCODING = "UTF-8";
public static final String[] TEXT_ENCODINGS = {"ISO-8859-1", "UTF-8", "GBK", "Big5", "ISO-2022-JP", "SHIFT_JS", "EUC-JP", "EUC-KR"};
public static final String INTENT_ORIGIN = "URL_INTENT_ORIGIN";
}
@@ -3,27 +3,32 @@
*/
package acr.browser.lightning.constant;
import android.app.Application;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.List;
import android.content.Context;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.database.HistoryItem;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.database.HistoryDatabase;
import acr.browser.lightning.database.HistoryItem;
import acr.browser.lightning.utils.Utils;
import acr.browser.lightning.view.LightningView;
public class HistoryPage {
public class HistoryPage extends AsyncTask<Void, Void, Void> {
public static final String FILENAME = "history.html";
private static final String HEADING = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\"><head><meta content=\"en-us\" http-equiv=\"Content-Language\" /><meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\"><title>"
+ BrowserApp.getAppContext().getString(R.string.action_history)
+ "</title></head><style>body { background: #e1e1e1;}.box { vertical-align:middle;position:relative; display: block; margin: 10px;padding-left:10px;padding-right:10px;padding-top:5px;padding-bottom:5px; background-color:#fff;box-shadow: 0px 2px 3px rgba( 0, 0, 0, 0.25 );font-family: Arial;color: #444;font-size: 12px;-moz-border-radius: 2px;-webkit-border-radius: 2px;border-radius: 2px;}.box a { width: 100%; height: 100%; position: absolute; left: 0; top: 0;}.black {color: black;font-size: 15px;font-family: Arial; white-space: nowrap; overflow: hidden;margin:auto; text-overflow: ellipsis; -o-text-overflow: ellipsis; -ms-text-overflow: ellipsis;}.font {color: gray;font-size: 10px;font-family: Arial; white-space: nowrap; overflow: hidden;margin:auto; text-overflow: ellipsis; -o-text-overflow: ellipsis; -ms-text-overflow: ellipsis;}</style><body><div id=\"content\">";
private static final String HEADING_1 = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\"><head><meta content=\"en-us\" http-equiv=\"Content-Language\" /><meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\"><title>";
private static final String HEADING_2 = "</title></head><style>body { background: #e1e1e1;}.box { vertical-align:middle;position:relative; display: block; margin: 10px;padding-left:10px;padding-right:10px;padding-top:5px;padding-bottom:5px; background-color:#fff;box-shadow: 0px 2px 3px rgba( 0, 0, 0, 0.25 );font-family: Arial;color: #444;font-size: 12px;-moz-border-radius: 2px;-webkit-border-radius: 2px;border-radius: 2px;}.box a { width: 100%; height: 100%; position: absolute; left: 0; top: 0;}.black {color: black;font-size: 15px;font-family: Arial; white-space: nowrap; overflow: hidden;margin:auto; text-overflow: ellipsis; -o-text-overflow: ellipsis; -ms-text-overflow: ellipsis;}.font {color: gray;font-size: 10px;font-family: Arial; white-space: nowrap; overflow: hidden;margin:auto; text-overflow: ellipsis; -o-text-overflow: ellipsis; -ms-text-overflow: ellipsis;}</style><body><div id=\"content\">";
private static final String PART1 = "<div class=\"box\"><a href=\"";
@@ -35,26 +40,58 @@ public class HistoryPage {
private static final String END = "</div></body></html>";
public static String getHistoryPage(Context context) {
StringBuilder historyBuilder = new StringBuilder(HistoryPage.HEADING);
List<HistoryItem> historyList = getWebHistory(context);
@NonNull private final WeakReference<LightningView> mTabReference;
@NonNull private final Application mApp;
@NonNull private final String mTitle;
private final HistoryDatabase mHistoryDatabase;
@Nullable private String mHistoryUrl = null;
public HistoryPage(LightningView tab, @NonNull Application app, HistoryDatabase database) {
mTabReference = new WeakReference<>(tab);
mApp = app;
mTitle = app.getString(R.string.action_history);
mHistoryDatabase = database;
}
@Nullable
@Override
protected Void doInBackground(Void... params) {
mHistoryUrl = getHistoryPage();
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
LightningView tab = mTabReference.get();
if (tab != null && mHistoryUrl != null) {
tab.loadUrl(mHistoryUrl);
}
}
@NonNull
private String getHistoryPage() {
StringBuilder historyBuilder = new StringBuilder(HEADING_1 + mTitle + HEADING_2);
List<HistoryItem> historyList = mHistoryDatabase.getLastHundredItems();
Iterator<HistoryItem> it = historyList.iterator();
HistoryItem helper;
while (it.hasNext()) {
helper = it.next();
historyBuilder.append(HistoryPage.PART1);
historyBuilder.append(PART1);
historyBuilder.append(helper.getUrl());
historyBuilder.append(HistoryPage.PART2);
historyBuilder.append(PART2);
historyBuilder.append(helper.getTitle());
historyBuilder.append(HistoryPage.PART3);
historyBuilder.append(PART3);
historyBuilder.append(helper.getUrl());
historyBuilder.append(HistoryPage.PART4);
historyBuilder.append(PART4);
}
historyBuilder.append(HistoryPage.END);
File historyWebPage = new File(context.getFilesDir(), FILENAME);
historyBuilder.append(END);
File historyWebPage = new File(mApp.getFilesDir(), FILENAME);
FileWriter historyWriter = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
historyWriter = new FileWriter(historyWebPage, false);
historyWriter.write(historyBuilder.toString());
} catch (IOException e) {
@@ -65,8 +102,22 @@ public class HistoryPage {
return Constants.FILE + historyWebPage;
}
private static List<HistoryItem> getWebHistory(Context context) {
HistoryDatabase databaseHandler = HistoryDatabase.getInstance();
return databaseHandler.getLastHundredItems();
public void load() {
executeOnExecutor(BrowserApp.getIOThread());
}
}
/**
* Use this method to immediately delete the history
* page on the current thread. This will clear the
* cached history page that was stored on file.
*
* @param application the application object needed to get the file.
*/
public static void deleteHistoryPage(@NonNull Application application) {
File historyWebPage = new File(application.getFilesDir(), FILENAME);
if (historyWebPage.exists()) {
historyWebPage.delete();
}
}
}
@@ -3,29 +3,36 @@
*/
package acr.browser.lightning.constant;
import android.app.Activity;
import android.app.Application;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.ref.WeakReference;
import javax.inject.Inject;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.Utils;
import acr.browser.lightning.view.LightningView;
public class StartPage {
public class StartPage extends AsyncTask<Void, Void, Void> {
private static final String FILENAME = "homepage.html";
public static final String FILENAME = "homepage.html";
private static final String HEAD = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\">"
private static final String HEAD_1 = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\">"
+ "<head>"
+ "<meta content=\"en-us\" http-equiv=\"Content-Language\" />"
+ "<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\" />"
+ "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">"
+ "<title>"
+ BrowserApp.getAppContext().getString(R.string.home)
+ "</title>"
+ "<title>";
private static final String HEAD_2 = "</title>"
+ "</head>"
+ "<style>body{background:#f2f2f2;text-align:center;margin:0px;}#search_input{height:35px; "
+ "width:100%;outline:none;border:none;font-size: 16px;background-color:transparent;}"
@@ -41,27 +48,59 @@ public class StartPage {
+ "font-size: 12px;-moz-border-radius: 2px;-webkit-border-radius: 2px;"
+ "border-radius: 2px;}</style><body> <div class=\"outer\"><div class=\"middle\"><div class=\"inner\"><img class=\"smaller\" src=\"";
private static final String MIDDLE = "\" ></br></br><form onsubmit=\"return search()\" class=\"search_bar\">"
private static final String MIDDLE = "\" ></br></br><form onsubmit=\"return search()\" class=\"search_bar\" autocomplete=\"off\">"
+ "<input type=\"submit\" id=\"search_submit\" value=\"Search\" ><span><input class=\"search\" type=\"text\" value=\"\" id=\"search_input\" >"
+ "</span></form></br></br></div></div></div><script type=\"text/javascript\">function search(){if(document.getElementById(\"search_input\").value != \"\"){window.location.href = \"";
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
*/
public static String getHomepage(Activity activity) {
StringBuilder homepageBuilder = new StringBuilder(StartPage.HEAD);
@NonNull
private String getHomepage() {
StringBuilder homepageBuilder = new StringBuilder(HEAD_1 + mTitle + HEAD_2);
String icon;
String searchUrl;
switch (PreferenceManager.getInstance().getSearchChoice()) {
switch (mPreferenceManager.getSearchChoice()) {
case 0:
// CUSTOM SEARCH
icon = "file:///android_asset/lightning.png";
searchUrl = PreferenceManager.getInstance().getSearchUrl();
searchUrl = mPreferenceManager.getSearchUrl();
break;
case 1:
// GOOGLE_SEARCH;
@@ -88,14 +127,14 @@ public class StartPage {
break;
case 5:
// STARTPAGE_SEARCH;
icon = "file:///android_asset/startpage.png";
// "https://startpage.com/graphics/startp_logo.gif";
icon = "file:///android_asset/png";
// "https://com/graphics/startp_logo.gif";
searchUrl = Constants.STARTPAGE_SEARCH;
break;
case 6:
// STARTPAGE_MOBILE
icon = "file:///android_asset/startpage.png";
// "https://startpage.com/graphics/startp_logo.gif";
icon = "file:///android_asset/png";
// "https://com/graphics/startp_logo.gif";
searchUrl = Constants.STARTPAGE_MOBILE_SEARCH;
break;
case 7:
@@ -131,13 +170,14 @@ public class StartPage {
}
homepageBuilder.append(icon);
homepageBuilder.append(StartPage.MIDDLE);
homepageBuilder.append(MIDDLE);
homepageBuilder.append(searchUrl);
homepageBuilder.append(StartPage.END);
homepageBuilder.append(END);
File homepage = new File(activity.getFilesDir(), StartPage.FILENAME);
File homepage = new File(mApp.getFilesDir(), FILENAME);
FileWriter hWriter = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
hWriter = new FileWriter(homepage, false);
hWriter.write(homepageBuilder.toString());
} catch (IOException e) {
@@ -148,4 +188,9 @@ public class StartPage {
return Constants.FILE + homepage;
}
public void load() {
executeOnExecutor(BrowserApp.getIOThread());
}
}
@@ -1,58 +0,0 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.controller;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Message;
import android.view.View;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient.CustomViewCallback;
import android.webkit.WebView;
import acr.browser.lightning.view.LightningView;
public interface BrowserController {
void updateUrl(String title, boolean shortUrl);
void updateProgress(int n);
void updateHistory(String title, String url);
void openFileChooser(ValueCallback<Uri> uploadMsg);
void updateTabs();
void onLongPress();
void onShowCustomView(View view, CustomViewCallback callback);
void onHideCustomView();
Bitmap getDefaultVideoPoster();
View getVideoLoadingProgressView();
void onCreateWindow(Message resultMsg);
void onCloseWindow(LightningView view);
void hideActionBar();
void showActionBar();
void longClickPage(String url);
void openBookmarkPage(WebView view);
void showFileChooser(ValueCallback<Uri[]> filePathCallback);
void closeEmptyTab();
boolean proxyIsNotReady();
// void updateBookmarkIndicator(String url);
}
@@ -0,0 +1,67 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.controller;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Message;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient.CustomViewCallback;
import acr.browser.lightning.activity.TabsManager;
import acr.browser.lightning.view.LightningView;
public interface UIController {
void changeToolbarBackground(@NonNull Bitmap favicon, @Nullable Drawable drawable);
@ColorInt
int getUiColor();
boolean getUseDarkTheme();
void updateUrl(@Nullable String title, boolean shortUrl);
void updateProgress(int n);
void updateHistory(@Nullable String title, @NonNull String url);
void openFileChooser(ValueCallback<Uri> uploadMsg);
void onShowCustomView(View view, CustomViewCallback callback);
void onShowCustomView(View view, CustomViewCallback callback, int requestedOrienation);
void onHideCustomView();
void onCreateWindow(Message resultMsg);
void onCloseWindow(LightningView view);
void hideActionBar();
void showActionBar();
void showFileChooser(ValueCallback<Uri[]> filePathCallback);
void closeEmptyTab();
void showCloseDialog(int position);
void newTabClicked();
void setForwardButtonEnabled(boolean enabled);
void setBackButtonEnabled(boolean enabled);
void tabChanged(LightningView tab);
TabsManager getTabModel();
}
@@ -11,6 +11,9 @@ 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 {
@@ -19,127 +22,185 @@ public class BookmarkLocalSync {
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";
private final Context mContext;
@NonNull private final Context mContext;
public BookmarkLocalSync(Context context) {
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() {
List<HistoryItem> list = new ArrayList<>();
if (!isStockSupported()) {
return list;
}
Cursor cursor = getStockCursor();
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);
}
list.add(new HistoryItem(url, title));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
Utils.close(cursor);
return list;
return getBookmarksFromContentUri(STOCK_BOOKMARKS_CONTENT);
}
@NonNull
@WorkerThread
public List<HistoryItem> getBookmarksFromChrome() {
List<HistoryItem> list = new ArrayList<>();
if (!isChromeSupported()) {
return list;
}
Cursor cursor = getStockCursor();
try {
if (cursor != null) {
for (int n = 0; n < cursor.getColumnCount(); n++) {
Log.d(TAG, cursor.getColumnName(n));
}
return getBookmarksFromContentUri(CHROME_BOOKMARKS_CONTENT);
}
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);
}
list.add(new HistoryItem(url, title));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
Utils.close(cursor);
return list;
@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 isStockSupported() {
Cursor cursor = getStockCursor();
Utils.close(cursor);
return cursor != null;
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
public boolean isChromeSupported() {
Cursor cursor = getChromeCursor();
Utils.close(cursor);
return cursor != null;
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() {
Cursor cursor;
Uri uri = Uri.parse(CHROME_BOOKMARKS_CONTENT);
try {
cursor = mContext.getContentResolver().query(uri,
new String[]{COLUMN_URL, COLUMN_TITLE, COLUMN_BOOKMARK}, null, null, null);
} catch (IllegalArgumentException e) {
return null;
}
return cursor;
return getBrowserCursor(CHROME_BOOKMARKS_CONTENT);
}
@Nullable
@WorkerThread
private Cursor getStockCursor() {
Cursor cursor;
Uri uri = Uri.parse(STOCK_BOOKMARKS_CONTENT);
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,
new String[]{COLUMN_URL, COLUMN_TITLE, COLUMN_BOOKMARK}, null, null, null);
} catch (IllegalArgumentException e) {
return null;
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();
}
return cursor;
}
}
@@ -32,6 +32,7 @@ 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;
@@ -49,29 +50,20 @@ public class BookmarkManager {
private static final String ORDER = "order";
private static final String FILE_BOOKMARKS = "bookmarks.dat";
private final String DEFAULT_BOOKMARK_TITLE;
@NonNull private final String DEFAULT_BOOKMARK_TITLE;
private Map<String, HistoryItem> mBookmarksMap;
// private final List<HistoryItem> mBookmarkList = new ArrayList<>();
private String mCurrentFolder = "";
private final ExecutorService mExecutor;
private boolean mReady = false;
private final File mFilesDir;
@NonNull private String mCurrentFolder = "";
@NonNull private final ExecutorService mExecutor;
private File mFilesDir;
public BookmarkManager(Context context) {
@Inject
public BookmarkManager(@NonNull Context context) {
mExecutor = Executors.newSingleThreadExecutor();
mFilesDir = context.getFilesDir();
DEFAULT_BOOKMARK_TITLE = context.getString(R.string.untitled);
mExecutor.execute(new BookmarkInitializer(context));
}
/**
* @return true if the BookmarkManager was initialized, false otherwise
*/
public boolean isReady() {
return mReady;
}
/**
* Look for bookmark using the url
*
@@ -97,6 +89,7 @@ public class BookmarkManager {
@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);
@@ -104,10 +97,12 @@ public class BookmarkManager {
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) {
@@ -132,7 +127,6 @@ public class BookmarkManager {
Utils.close(inputStream);
}
mBookmarksMap = bookmarks;
mReady = true;
}
}
@@ -152,11 +146,12 @@ public class BookmarkManager {
@Override
public void run() {
final File tempFile = new File(mFilesDir,
String.format("bm_%d.dat", System.currentTimeMillis()));
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) {
@@ -168,7 +163,7 @@ public class BookmarkManager {
bookmarkWriter.newLine();
}
success = true;
} catch (IOException | JSONException e) {
} catch (@NonNull IOException | JSONException e) {
e.printStackTrace();
} finally {
Utils.close(bookmarkWriter);
@@ -176,6 +171,7 @@ public class BookmarkManager {
if (success) {
// Overwrite the bookmarks file by renaming the temp file
//noinspection ResultOfMethodCallIgnored
tempFile.renameTo(bookmarksFile);
}
}
@@ -200,7 +196,7 @@ public class BookmarkManager {
*/
public synchronized boolean addBookmark(@NonNull HistoryItem item) {
final String url = item.getUrl();
if (url == null || mBookmarksMap.containsKey(url)) {
if (mBookmarksMap.containsKey(url)) {
return false;
}
mBookmarksMap.put(url, item);
@@ -213,13 +209,13 @@ public class BookmarkManager {
*
* @param list the list of HistoryItems to add to bookmarks
*/
public synchronized void addBookmarkList(List<HistoryItem> list) {
public synchronized void addBookmarkList(@Nullable List<HistoryItem> list) {
if (list == null || list.isEmpty()) {
return;
}
for (HistoryItem item : list) {
final String url = item.getUrl();
if (url != null && !mBookmarksMap.containsKey(url)) {
if (!mBookmarksMap.containsKey(url)) {
mBookmarksMap.put(url, item);
}
}
@@ -232,7 +228,7 @@ public class BookmarkManager {
*
* @param deleteItem the bookmark item to delete
*/
public synchronized boolean deleteBookmark(HistoryItem deleteItem) {
public synchronized boolean deleteBookmark(@Nullable HistoryItem deleteItem) {
if (deleteItem == null || deleteItem.isFolder()) {
return false;
}
@@ -286,13 +282,23 @@ public class BookmarkManager {
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(HistoryItem oldItem, HistoryItem newItem) {
public synchronized void editBookmark(@Nullable HistoryItem oldItem, @Nullable HistoryItem newItem) {
if (oldItem == null || newItem == null || oldItem.isFolder()) {
return;
}
@@ -317,7 +323,7 @@ public class BookmarkManager {
* This method exports the stored bookmarks to a text file in the device's
* external download directory
*/
public synchronized void exportBookmarks(Activity activity) {
public synchronized void exportBookmarks(@NonNull Activity activity) {
List<HistoryItem> bookmarkList = getAllBookmarks(true);
File bookmarksExport = new File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
@@ -331,6 +337,7 @@ public class BookmarkManager {
}
BufferedWriter bookmarkWriter = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
bookmarkWriter = new BufferedWriter(new FileWriter(bookmarksExport,
false));
JSONObject object = new JSONObject();
@@ -344,7 +351,7 @@ public class BookmarkManager {
}
Utils.showSnackbar(activity, activity.getString(R.string.bookmark_export_path)
+ ' ' + bookmarksExport.getPath());
} catch (IOException | JSONException e) {
} catch (@NonNull IOException | JSONException e) {
e.printStackTrace();
} finally {
Utils.close(bookmarkWriter);
@@ -360,6 +367,7 @@ public class BookmarkManager {
* @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) {
@@ -377,8 +385,9 @@ public class BookmarkManager {
* @param folder the name of the folder to retrieve bookmarks from
* @return a list of bookmarks found in that folder
*/
public synchronized List<HistoryItem> getBookmarksFromFolder(String folder, boolean sort) {
List<HistoryItem> bookmarks = new ArrayList<>();
@NonNull
public synchronized List<HistoryItem> getBookmarksFromFolder(@Nullable String folder, boolean sort) {
List<HistoryItem> bookmarks = new ArrayList<>(1);
if (folder == null || folder.isEmpty()) {
bookmarks.addAll(getFolders(sort));
folder = "";
@@ -394,6 +403,36 @@ public class BookmarkManager {
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
*
@@ -408,24 +447,11 @@ public class BookmarkManager {
*
* @return the current folder
*/
@Nullable
public String getCurrentFolder() {
return mCurrentFolder;
}
/**
* Method is used internally for searching the bookmarks
*
* @return a sorted map of all bookmarks, useful for seeing if a bookmark exists
*/
private Set<String> getBookmarkUrls(List<HistoryItem> list) {
Set<String> set = new HashSet<>();
for (HistoryItem item : mBookmarksMap.values()) {
if (!item.isFolder())
set.add(item.getUrl());
}
return set;
}
/**
* This method returns a list of all folders.
* Folders cannot be empty as they are generated from
@@ -433,11 +459,12 @@ public class BookmarkManager {
*
* @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 != null && !folderName.isEmpty() && !folders.containsKey(folderName)) {
if (!folderName.isEmpty() && !folders.containsKey(folderName)) {
final HistoryItem folder = new HistoryItem();
folder.setIsFolder(true);
folder.setTitle(folderName);
@@ -459,11 +486,12 @@ public class BookmarkManager {
*
* @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 != null && !folderName.isEmpty()) {
if (!folderName.isEmpty()) {
folders.add(folderName);
}
}
@@ -476,13 +504,14 @@ public class BookmarkManager {
*
* @param file the file to attempt to import bookmarks from
*/
public synchronized void importBookmarksFromFile(File file, Activity activity) {
public synchronized void importBookmarksFromFile(@Nullable File file, @NonNull Activity activity) {
if (file == null) {
return;
}
List<HistoryItem> list = new ArrayList<>();
BufferedReader bookmarksReader = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
bookmarksReader = new BufferedReader(new FileReader(file));
String line;
int number = 0;
@@ -499,7 +528,7 @@ public class BookmarkManager {
addBookmarkList(list);
String message = activity.getResources().getString(R.string.message_import);
Utils.showSnackbar(activity, number + " " + message);
} catch (IOException | JSONException e) {
} catch (@NonNull IOException | JSONException e) {
e.printStackTrace();
Utils.createInformativeDialog(activity, R.string.title_error, R.string.import_bookmark_error);
} finally {
@@ -507,29 +536,13 @@ public class BookmarkManager {
}
}
/**
* find the index of a bookmark in a list using only its URL
*
* @param list the list to search
* @param url the url to compare
* @return returns the index of the bookmark or -1 if none was found
*/
public static int getIndexOfBookmark(final List<HistoryItem> list, final String url) {
for (int n = 0; n < list.size(); n++) {
if (list.get(n).getUrl().equals(url)) {
return n;
}
}
return -1;
}
/**
* This class sorts bookmarks alphabetically, with folders coming after bookmarks
*/
public static class SortIgnoreCase implements Comparator<HistoryItem> {
private static class SortIgnoreCase implements Comparator<HistoryItem> {
public int compare(HistoryItem o1, HistoryItem o2) {
if (o1 == null || o2 == null || o1.getTitle() == null || o2.getTitle() == null) {
public int compare(@Nullable HistoryItem o1, @Nullable HistoryItem o2) {
if (o1 == null || o2 == null) {
return 0;
}
if (o1.isFolder() == o2.isFolder()) {
@@ -8,13 +8,19 @@ import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import 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
@@ -33,27 +39,28 @@ public class HistoryDatabase extends SQLiteOpenHelper {
private static final String KEY_TITLE = "title";
private static final String KEY_TIME_VISITED = "time";
private SQLiteDatabase mDatabase;
@Nullable private SQLiteDatabase mDatabase;
private static HistoryDatabase mInstance;
private boolean mLock;
public static HistoryDatabase getInstance() {
if (mInstance == null || mInstance.isClosed()) {
mInstance = new HistoryDatabase(BrowserApp.getAppContext());
}
return mInstance;
@Inject
public HistoryDatabase(@NonNull Context context) {
super(context.getApplicationContext(), DATABASE_NAME, null, DATABASE_VERSION);
initialize();
}
private HistoryDatabase(Context context) {
super(context.getApplicationContext(), DATABASE_NAME, null, DATABASE_VERSION);
mDatabase = this.getWritableDatabase();
private void initialize() {
BrowserApp.getTaskThread().execute(new Runnable() {
@Override
public void run() {
synchronized (HistoryDatabase.this) {
mDatabase = HistoryDatabase.this.getWritableDatabase();
}
}
});
}
// Creating Tables
@Override
public void onCreate(SQLiteDatabase db) {
public void onCreate(@NonNull SQLiteDatabase db) {
String CREATE_HISTORY_TABLE = "CREATE TABLE " + TABLE_HISTORY + '(' + KEY_ID
+ " INTEGER PRIMARY KEY," + KEY_URL + " TEXT," + KEY_TITLE + " TEXT,"
+ KEY_TIME_VISITED + " INTEGER" + ')';
@@ -62,78 +69,69 @@ public class HistoryDatabase extends SQLiteOpenHelper {
// Upgrading database
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) {
// Drop older table if it exists
db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY);
// Create tables again
onCreate(db);
}
public void deleteHistory() {
public synchronized void deleteHistory() {
mDatabase = openIfNecessary();
mDatabase.delete(TABLE_HISTORY, null, null);
mDatabase.close();
mDatabase = this.getWritableDatabase();
}
public boolean isClosed() {
return mDatabase == null || !mDatabase.isOpen();
}
@Override
public synchronized void close() {
if (!mLock) {
if (mDatabase != null) {
mDatabase.close();
mDatabase = null;
}
if (mDatabase != null) {
mDatabase.close();
mDatabase = null;
}
super.close();
}
private void openIfNecessary() {
if (mDatabase == null) {
@NonNull
private SQLiteDatabase openIfNecessary() {
if (mDatabase == null || !mDatabase.isOpen()) {
mDatabase = this.getWritableDatabase();
}
return mDatabase;
}
public synchronized void deleteHistoryItem(String url) {
mLock = true;
openIfNecessary();
public synchronized void deleteHistoryItem(@NonNull String url) {
mDatabase = openIfNecessary();
mDatabase.delete(TABLE_HISTORY, KEY_URL + " = ?", new String[]{url});
mLock = false;
}
public synchronized void visitHistoryItem(String url, String title) {
mLock = true;
openIfNecessary();
public synchronized void visitHistoryItem(@NonNull String url, @Nullable String title) {
mDatabase = openIfNecessary();
ContentValues values = new ContentValues();
values.put(KEY_TITLE, title);
values.put(KEY_TITLE, title == null ? "" : title);
values.put(KEY_TIME_VISITED, System.currentTimeMillis());
Cursor q = mDatabase.query(false, TABLE_HISTORY, new String[]{KEY_URL},
KEY_URL + " = ?", new String[]{url}, null, null, null, "1");
if (q.getCount() > 0) {
mDatabase.update(TABLE_HISTORY, values, KEY_URL + " = ?", new String[]{url});
} else {
addHistoryItem(new HistoryItem(url, title));
addHistoryItem(new HistoryItem(url, title == null ? "" : title));
}
q.close();
mLock = false;
}
private synchronized void addHistoryItem(HistoryItem item) {
mLock = true;
openIfNecessary();
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);
mLock = false;
}
String getHistoryItem(String url) {
mLock = true;
openIfNecessary();
@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;
@@ -143,14 +141,16 @@ public class HistoryDatabase extends SQLiteOpenHelper {
cursor.close();
}
mLock = false;
return m;
}
public List<HistoryItem> findItemsContaining(String search) {
mLock = true;
openIfNecessary();
@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";
@@ -168,13 +168,12 @@ public class HistoryDatabase extends SQLiteOpenHelper {
} while (cursor.moveToNext() && n < 5);
}
cursor.close();
mLock = false;
return itemList;
}
public List<HistoryItem> getLastHundredItems() {
mLock = true;
openIfNecessary();
@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";
@@ -192,13 +191,12 @@ public class HistoryDatabase extends SQLiteOpenHelper {
} while (cursor.moveToNext() && counter < 100);
}
cursor.close();
mLock = false;
return itemList;
}
public List<HistoryItem> getAllHistoryItems() {
mLock = true;
openIfNecessary();
@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";
@@ -215,18 +213,15 @@ public class HistoryDatabase extends SQLiteOpenHelper {
} while (cursor.moveToNext());
}
cursor.close();
mLock = false;
return itemList;
}
public int getHistoryItemsCount() {
mLock = true;
openIfNecessary();
public synchronized int getHistoryItemsCount() {
mDatabase = openIfNecessary();
String countQuery = "SELECT * FROM " + TABLE_HISTORY;
Cursor cursor = mDatabase.rawQuery(countQuery, null);
int n = cursor.getCount();
cursor.close();
mLock = false;
return n;
}
}
@@ -7,6 +7,8 @@ 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
@@ -26,10 +28,9 @@ public class HistoryItem implements Comparable<HistoryItem> {
private int mOrder = 0;
private boolean mIsFolder = false;
// Empty constructor
public HistoryItem() {}
public HistoryItem(HistoryItem item) {
public HistoryItem(@NonNull HistoryItem item) {
this.mUrl = item.mUrl;
this.mTitle = item.mTitle;
this.mFolder = item.mFolder;
@@ -37,15 +38,17 @@ public class HistoryItem implements Comparable<HistoryItem> {
this.mIsFolder = item.mIsFolder;
}
// constructor
public HistoryItem(@NonNull String url, @NonNull String title) {
Preconditions.checkNonNull(url);
Preconditions.checkNonNull(title);
this.mUrl = url;
this.mTitle = title;
this.mBitmap = null;
}
// constructor
public HistoryItem(@NonNull String url, @NonNull String title, int imageId) {
Preconditions.checkNonNull(url);
Preconditions.checkNonNull(title);
this.mUrl = url;
this.mTitle = title;
this.mBitmap = null;
@@ -64,7 +67,7 @@ public class HistoryItem implements Comparable<HistoryItem> {
mBitmap = image;
}
public void setFolder(String folder) {
public void setFolder(@Nullable String folder) {
mFolder = (folder == null) ? "" : folder;
}
@@ -76,31 +79,31 @@ public class HistoryItem implements Comparable<HistoryItem> {
return mOrder;
}
@NonNull
public String getFolder() {
return mFolder;
}
@Nullable
public Bitmap getBitmap() {
return mBitmap;
}
// getting name
@NonNull
public String getUrl() {
return this.mUrl;
}
// setting name
public void setUrl(String url) {
public void setUrl(@Nullable String url) {
this.mUrl = (url == null) ? "" : url;
}
// getting phone number
@NonNull
public String getTitle() {
return this.mTitle;
}
// setting phone number
public void setTitle(String title) {
public void setTitle(@Nullable String title) {
this.mTitle = (title == null) ? "" : title;
}
@@ -112,6 +115,7 @@ public class HistoryItem implements Comparable<HistoryItem> {
return mIsFolder;
}
@NonNull
@Override
public String toString() {
return mTitle;
@@ -127,7 +131,7 @@ public class HistoryItem implements Comparable<HistoryItem> {
}
@Override
public boolean equals(Object object) {
public boolean equals(@Nullable Object object) {
if (this == object) return true;
if (object == null) return false;
@@ -1,8 +1,12 @@
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;
@@ -19,69 +23,75 @@ 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 BookmarksDialogBuilder {
public class LightningDialogBuilder {
@Inject BookmarkManager mBookmarkManager;
@Inject PreferenceManager mPreferenceManager;
@Inject HistoryDatabase mHistoryDatabase;
@Inject Bus mEventBus;
@Inject
BookmarkManager bookmarkManager;
@Inject
Bus eventBus;
@Inject
public BookmarksDialogBuilder() {
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
*
* @param context used to show the dialog
* @param url the long pressed url
*/
public void showLongPressedDialogForUrl(final Context context, final String url) {
public void showLongPressedDialogForBookmarkUrl(@NonNull final Context context, @NonNull final String url) {
final HistoryItem item;
if (url.startsWith(Constants.FILE) && url.endsWith(Constants.BOOKMARKS_FILENAME)) {
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() - Constants.BOOKMARKS_FILENAME.length() - 1);
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 = bookmarkManager.findBookmarkForUrl(url);
item = mBookmarkManager.findBookmarkForUrl(url);
}
if (item != null) {
if (item.isFolder()) {
showBookmarkFolderLongPressedDialog(context, item);
} else {
showLongPressedDialogForUrl(context, item);
showLongPressedDialogForBookmarkUrl(context, item);
}
}
}
public void showLongPressedDialogForUrl(final Context context, final HistoryItem 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:
eventBus.post(new BookmarkEvents.AsNewTab(item));
mEventBus.post(new BrowserEvents.OpenUrlInNewTab(item.getUrl()));
break;
case DialogInterface.BUTTON_NEGATIVE:
if (bookmarkManager.deleteBookmark(item)) {
eventBus.post(new BookmarkEvents.Deleted(item));
if (mBookmarkManager.deleteBookmark(item)) {
mEventBus.post(new BookmarkEvents.Deleted(item));
}
break;
case DialogInterface.BUTTON_NEUTRAL:
@@ -101,7 +111,7 @@ public class BookmarksDialogBuilder {
.show();
}
private void showEditBookmarkDialog(final Context context, final HistoryItem item) {
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);
@@ -113,7 +123,7 @@ public class BookmarksDialogBuilder {
(AutoCompleteTextView) dialogLayout.findViewById(R.id.bookmark_folder);
getFolder.setHint(R.string.folder);
getFolder.setText(item.getFolder());
final List<String> folders = bookmarkManager.getFolderTitles();
final List<String> folders = mBookmarkManager.getFolderTitles();
final ArrayAdapter<String> suggestionsAdapter = new ArrayAdapter<>(context,
android.R.layout.simple_dropdown_item_1line, folders);
getFolder.setThreshold(1);
@@ -129,14 +139,14 @@ public class BookmarksDialogBuilder {
editedItem.setUrl(getUrl.getText().toString());
editedItem.setUrl(getUrl.getText().toString());
editedItem.setFolder(getFolder.getText().toString());
bookmarkManager.editBookmark(item, editedItem);
eventBus.post(new BookmarkEvents.BookmarkChanged(item, editedItem));
mBookmarkManager.editBookmark(item, editedItem);
mEventBus.post(new BookmarkEvents.BookmarkChanged(item, editedItem));
}
});
editBookmarkDialog.show();
}
public void showBookmarkFolderLongPressedDialog(final Context context, final HistoryItem item) {
public void showBookmarkFolderLongPressedDialog(@NonNull final Context context, @NonNull final HistoryItem item) {
// assert item.isFolder();
final DialogInterface.OnClickListener dialogClickListener =
new DialogInterface.OnClickListener() {
@@ -148,9 +158,9 @@ public class BookmarksDialogBuilder {
break;
case DialogInterface.BUTTON_NEGATIVE:
bookmarkManager.deleteFolder(item.getTitle());
mBookmarkManager.deleteFolder(item.getTitle());
// setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false);
eventBus.post(new BookmarkEvents.Deleted(item));
mEventBus.post(new BookmarkEvents.Deleted(item));
break;
}
}
@@ -165,7 +175,7 @@ public class BookmarksDialogBuilder {
.show();
}
private void showRenameFolderDialog(final Context context, final HistoryItem item) {
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);
@@ -191,10 +201,105 @@ public class BookmarksDialogBuilder {
editedItem.setUrl(Constants.FOLDER + newTitle);
editedItem.setFolder(item.getFolder());
editedItem.setIsFolder(true);
bookmarkManager.renameFolder(oldTitle, newTitle);
eventBus.post(new BookmarkEvents.BookmarkChanged(item, editedItem));
mBookmarkManager.renameFolder(oldTitle, newTitle);
mEventBus.post(new BookmarkEvents.BookmarkChanged(item, editedItem));
}
});
editFolderDialog.show();
}
public void showLongPressedHistoryLinkDialog(final Context context, @NonNull final String url) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url));
break;
case DialogInterface.BUTTON_NEGATIVE:
mHistoryDatabase.deleteHistoryItem(url);
// openHistory();
mEventBus.post(new BrowserEvents.OpenHistoryInCurrentTab());
break;
case DialogInterface.BUTTON_NEUTRAL:
mEventBus.post(new BrowserEvents.OpenUrlInCurrentTab(url));
break;
default:
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.action_history)
.setMessage(R.string.dialog_history_long_press)
.setCancelable(true)
.setPositiveButton(R.string.action_new_tab, dialogClickListener)
.setNegativeButton(R.string.action_delete, dialogClickListener)
.setNeutralButton(R.string.action_open, dialogClickListener)
.show();
}
// TODO There should be a way in which we do not need an activity reference to dowload a file
public void showLongPressImageDialog(@NonNull final Activity activity, @NonNull final String url,
@NonNull final String userAgent) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url));
break;
case DialogInterface.BUTTON_NEGATIVE:
mEventBus.post(new BrowserEvents.OpenUrlInCurrentTab(url));
break;
case DialogInterface.BUTTON_NEUTRAL:
Utils.downloadFile(activity, mPreferenceManager, url, userAgent, "attachment");
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(url.replace(Constants.HTTP, ""))
.setCancelable(true)
.setMessage(R.string.dialog_image)
.setPositiveButton(R.string.action_new_tab, dialogClickListener)
.setNegativeButton(R.string.action_open, dialogClickListener)
.setNeutralButton(R.string.action_download, dialogClickListener)
.show();
}
public void showLongPressLinkDialog(@NonNull final Context context, final String url) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url));
break;
case DialogInterface.BUTTON_NEGATIVE:
mEventBus.post(new BrowserEvents.OpenUrlInCurrentTab(url));
break;
case DialogInterface.BUTTON_NEUTRAL:
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("label", url);
clipboard.setPrimaryClip(clip);
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(context); // dialog
builder.setTitle(url)
.setCancelable(true)
.setMessage(R.string.dialog_link)
.setPositiveButton(R.string.action_new_tab, dialogClickListener)
.setNegativeButton(R.string.action_open, dialogClickListener)
.setNeutralButton(R.string.action_copy, dialogClickListener)
.show();
}
}
@@ -3,29 +3,35 @@
*/
package acr.browser.lightning.download;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.URLUtil;
import com.squareup.otto.Bus;
import java.io.File;
import java.io.IOException;
import acr.browser.lightning.BuildConfig;
import acr.browser.lightning.R;
import acr.browser.lightning.activity.MainActivity;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.bus.BrowserEvents;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.Utils;
/**
* Handle download requests
@@ -44,14 +50,14 @@ public class DownloadHandler {
* Notify the host application a download should be done, or that the data
* should be streamed if a streaming viewer is available.
*
* @param activity Activity requesting the download.
* @param context The context in which the download was requested.
* @param url The full url to the content that should be downloaded
* @param userAgent User agent of the downloading application.
* @param contentDisposition Content-disposition http header, if present.
* @param mimetype The mimetype of the content reported by the server
*/
public static void onDownloadStart(Activity activity, String url, String userAgent,
String contentDisposition, String mimetype) {
public static void onDownloadStart(@NonNull Context context, @NonNull PreferenceManager manager, String url, String userAgent,
@Nullable String contentDisposition, String mimetype) {
// if we're dealing wih A/V content that's not explicitly marked
// for download, check if it's streamable.
if (contentDisposition == null
@@ -61,18 +67,22 @@ public class DownloadHandler {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(url), mimetype);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ResolveInfo info = activity.getPackageManager().resolveActivity(intent,
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
intent.setSelector(null);
}
ResolveInfo info = context.getPackageManager().resolveActivity(intent,
PackageManager.MATCH_DEFAULT_ONLY);
if (info != null) {
ComponentName myName = activity.getComponentName();
// If we resolved to ourselves, we don't want to attempt to
// load the url only to try and download it again.
if (!myName.getPackageName().equals(info.activityInfo.packageName)
|| !myName.getClassName().equals(info.activityInfo.name)) {
if (BuildConfig.APPLICATION_ID.equals(info.activityInfo.packageName)
|| MainActivity.class.getName().equals(info.activityInfo.name)) {
// someone (other than us) knows how to handle this mime
// type with this scheme, don't download.
try {
activity.startActivity(intent);
context.startActivity(intent);
return;
} catch (ActivityNotFoundException ex) {
// Best behavior is to fall back to a download in this
@@ -81,14 +91,14 @@ public class DownloadHandler {
}
}
}
onDownloadStartNoStream(activity, url, userAgent, contentDisposition, mimetype
);
onDownloadStartNoStream(context, manager, url, userAgent, contentDisposition, mimetype);
}
// This is to work around the fact that java.net.URI throws Exceptions
// instead of just encoding URL's properly
// Helper method for onDownloadStartNoStream
private static String encodePath(String path) {
@NonNull
private static String encodePath(@NonNull String path) {
char[] chars = path.toCharArray();
boolean needed = false;
@@ -119,17 +129,18 @@ public class DownloadHandler {
* Notify the host application a download should be done, even if there is a
* streaming viewer available for thise type.
*
* @param activity Activity requesting the download.
* @param context The context in which the download is requested.
* @param url The full url to the content that should be downloaded
* @param userAgent User agent of the downloading application.
* @param contentDisposition Content-disposition http header, if present.
* @param mimetype The mimetype of the content reported by the server
*/
/* package */
private static void onDownloadStartNoStream(final Activity activity, String url, String userAgent,
String contentDisposition, String mimetype) {
String filename = URLUtil.guessFileName(url, contentDisposition, mimetype);
private static void onDownloadStartNoStream(@NonNull final Context context, @NonNull PreferenceManager preferences,
String url, String userAgent,
String contentDisposition, @Nullable String mimetype) {
final Bus eventBus = BrowserApp.getBus(context);
final String filename = URLUtil.guessFileName(url, contentDisposition, mimetype);
// Check to see if we have an SDCard
String status = Environment.getExternalStorageState();
@@ -139,14 +150,14 @@ public class DownloadHandler {
// Check to see if the SDCard is busy, same as the music app
if (status.equals(Environment.MEDIA_SHARED)) {
msg = activity.getString(R.string.download_sdcard_busy_dlg_msg);
msg = context.getString(R.string.download_sdcard_busy_dlg_msg);
title = R.string.download_sdcard_busy_dlg_title;
} else {
msg = activity.getString(R.string.download_no_sdcard_dlg_msg, filename);
msg = context.getString(R.string.download_no_sdcard_dlg_msg);
title = R.string.download_no_sdcard_dlg_title;
}
new AlertDialog.Builder(activity).setTitle(title)
new AlertDialog.Builder(context).setTitle(title)
.setIcon(android.R.drawable.ic_dialog_alert).setMessage(msg)
.setPositiveButton(R.string.action_ok, null).show();
return;
@@ -162,7 +173,7 @@ public class DownloadHandler {
// 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);
Utils.showSnackbar(activity, R.string.problem_download);
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.problem_download));
return;
}
@@ -172,7 +183,7 @@ public class DownloadHandler {
try {
request = new DownloadManager.Request(uri);
} catch (IllegalArgumentException e) {
Utils.showSnackbar(activity, R.string.cannot_download);
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.cannot_download));
return;
}
request.setMimeType(mimetype);
@@ -180,26 +191,20 @@ public class DownloadHandler {
// or, should it be set to one of several Environment.DIRECTORY* dirs
// depending on mimetype?
String location = PreferenceManager.getInstance().getDownloadDirectory();
String location = preferences.getDownloadDirectory();
Uri downloadFolder;
if (location != null) {
location = addNecessarySlashes(location);
downloadFolder = Uri.parse(location);
} else {
location = addNecessarySlashes(DEFAULT_DOWNLOAD_PATH);
downloadFolder = Uri.parse(location);
PreferenceManager.getInstance().setDownloadDirectory(location);
}
location = addNecessarySlashes(location);
downloadFolder = Uri.parse(location);
File dir = new File(downloadFolder.getPath());
if (!dir.isDirectory() && !dir.mkdirs()) {
// Cannot make the directory
Utils.showSnackbar(activity, R.string.problem_location_download);
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.problem_location_download));
return;
}
if (!isWriteAccessAvailable(downloadFolder)) {
Utils.showSnackbar(activity, R.string.problem_location_download);
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.problem_location_download));
return;
}
request.setDestinationUri(Uri.parse(Constants.FILE + location + filename));
@@ -214,32 +219,30 @@ public class DownloadHandler {
request.addRequestHeader(COOKIE_REQUEST_HEADER, cookies);
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
if (mimetype == null) {
Log.d(TAG, "Mimetype is null");
if (TextUtils.isEmpty(addressString)) {
return;
}
// We must have long pressed on a link or image to download it. We
// are not sure of the mimetype in this case, so do a head request
new FetchUrlMimeType(activity, request, addressString, cookies, userAgent).start();
new FetchUrlMimeType(context, request, addressString, cookies, userAgent).start();
} else {
final DownloadManager manager = (DownloadManager) activity
Log.d(TAG, "Valid mimetype, attempting to download");
final DownloadManager manager = (DownloadManager) context
.getSystemService(Context.DOWNLOAD_SERVICE);
new Thread() {
@Override
public void run() {
try {
manager.enqueue(request);
} catch (IllegalArgumentException e) {
// Probably got a bad URL or something
e.printStackTrace();
Utils.showSnackbar(activity, R.string.cannot_download);
} 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
Utils.showSnackbar(activity, R.string.problem_location_download);
}
}
}.start();
Utils.showSnackbar(activity, activity.getString(R.string.download_pending) + ' ' + filename);
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));
}
}
@@ -255,7 +258,7 @@ public class DownloadHandler {
* @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(String directory) {
public static boolean isWriteAccessAvailable(@Nullable String directory) {
if (directory == null || directory.isEmpty()) {
return false;
}
@@ -266,6 +269,7 @@ public class DownloadHandler {
if (!file.exists()) {
try {
if (file.createNewFile()) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
return true;
@@ -286,34 +290,38 @@ public class DownloadHandler {
* @param directory the directory to find the first existent parent
* @return the first existent parent
*/
private static String getFirstRealParentDirectory(String directory) {
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) {
return getFirstRealParentDirectory(parent.substring(0, previousIndex));
@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 "/";
return directory;
}
} else {
return directory;
}
}
private static boolean isWriteAccessAvailable(Uri fileUri) {
private static boolean isWriteAccessAvailable(@NonNull Uri fileUri) {
File file = new File(fileUri.getPath());
try {
if (file.createNewFile()) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
return true;
@@ -322,7 +330,8 @@ public class DownloadHandler {
}
}
public static String addNecessarySlashes(String originalPath) {
@NonNull
public static String addNecessarySlashes(@Nullable String originalPath) {
if (originalPath == null || originalPath.length() == 0) {
return "/";
}
@@ -3,19 +3,24 @@
*/
package acr.browser.lightning.download;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.Context;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.webkit.MimeTypeMap;
import android.webkit.URLUtil;
import com.squareup.otto.Bus;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import acr.browser.lightning.R;
import acr.browser.lightning.utils.Utils;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.bus.BrowserEvents;
/**
* This class is used to pull down the http headers of a given URL so that we
@@ -27,7 +32,7 @@ import acr.browser.lightning.utils.Utils;
*/
class FetchUrlMimeType extends Thread {
private final Activity mActivity;
private final Context mContext;
private final DownloadManager.Request mRequest;
@@ -37,9 +42,9 @@ class FetchUrlMimeType extends Thread {
private final String mUserAgent;
public FetchUrlMimeType(Activity activity, DownloadManager.Request request, String uri,
public FetchUrlMimeType(Context context, DownloadManager.Request request, String uri,
String cookies, String userAgent) {
mActivity = activity;
mContext = context;
mRequest = request;
mUri = uri;
mCookies = cookies;
@@ -50,6 +55,7 @@ class FetchUrlMimeType extends Thread {
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;
@@ -78,7 +84,7 @@ class FetchUrlMimeType extends Thread {
contentDisposition = contentDispositionHeader;
}
}
} catch (IllegalArgumentException | IOException ex) {
} catch (@NonNull IllegalArgumentException | IOException ex) {
if (connection != null)
connection.disconnect();
} finally {
@@ -101,9 +107,16 @@ class FetchUrlMimeType extends Thread {
}
// Start the download
DownloadManager manager = (DownloadManager) mActivity
DownloadManager manager = (DownloadManager) mContext
.getSystemService(Context.DOWNLOAD_SERVICE);
manager.enqueue(mRequest);
Utils.showSnackbar(mActivity, mActivity.getString(R.string.download_pending) + ' ' + filename);
Handler handler = new Handler(Looper.getMainLooper());
final String file = filename;
handler.post(new Runnable() {
@Override
public void run() {
eventBus.post(new BrowserEvents.ShowSnackBarMessage(mContext.getString(R.string.download_pending) + ' ' + file));
}
});
}
}
@@ -3,6 +3,7 @@
*/
package acr.browser.lightning.download;
import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.support.v7.app.AlertDialog;
@@ -11,43 +12,64 @@ import android.webkit.DownloadListener;
import android.webkit.URLUtil;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.preference.PreferenceManager;
import com.anthonycr.grant.PermissionsManager;
import com.anthonycr.grant.PermissionsResultAction;
import javax.inject.Inject;
public class LightningDownloadListener implements DownloadListener {
private final Activity mActivity;
public LightningDownloadListener(Activity activity) {
mActivity = activity;
@Inject PreferenceManager mPreferenceManager;
public LightningDownloadListener(Activity context) {
BrowserApp.getAppComponent().inject(this);
mActivity = context;
}
@Override
public void onDownloadStart(final String url, final String userAgent,
final String contentDisposition, final String mimetype, long contentLength) {
String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype);
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
DownloadHandler.onDownloadStart(mActivity, url, userAgent,
contentDisposition, mimetype);
break;
final String contentDisposition, final String mimetype, long contentLength) {
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(mActivity,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
new PermissionsResultAction() {
@Override
public void onGranted() {
String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype);
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
DownloadHandler.onDownloadStart(mActivity, mPreferenceManager, url, userAgent,
contentDisposition, mimetype);
break;
case DialogInterface.BUTTON_NEGATIVE:
break;
}
}
};
case DialogInterface.BUTTON_NEGATIVE:
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); // dialog
builder.setTitle(fileName)
.setMessage(mActivity.getResources().getString(R.string.dialog_download))
.setPositiveButton(mActivity.getResources().getString(R.string.action_download),
dialogClickListener)
.setNegativeButton(mActivity.getResources().getString(R.string.action_cancel),
dialogClickListener).show();
Log.i(Constants.TAG, "Downloading" + fileName);
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); // dialog
builder.setTitle(fileName)
.setMessage(mActivity.getResources().getString(R.string.dialog_download))
.setPositiveButton(mActivity.getResources().getString(R.string.action_download),
dialogClickListener)
.setNegativeButton(mActivity.getResources().getString(R.string.action_cancel),
dialogClickListener).show();
Log.i(Constants.TAG, "Downloading" + fileName);
}
@Override
public void onDenied(String permission) {
//TODO show message
}
});
}
}
@@ -4,6 +4,7 @@
package acr.browser.lightning.download;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Locale;
import java.util.regex.Matcher;
@@ -35,17 +36,17 @@ class WebAddress {
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$_.+!*'(),;?&=]+)?)@)?" +
/* scheme */"(?:(http|https|file)://)?" +
/* authority */"(?:([-A-Za-z0-9$_.+!*'(),;?&=]+(?::[-A-Za-z0-9$_.+!*'(),;?&=]+)?)@)?" +
/* host */"([" + GOOD_IRI_CHAR + "%_-][" + GOOD_IRI_CHAR + "%_\\.-]*|\\[[0-9a-fA-F:\\.]+\\])?" +
/* port */"(?:\\:([0-9]*))?" +
/* path */"(\\/?[^#]*)?" +
/* port */"(?::([0-9]*))?" +
/* path */"(/?[^#]*)?" +
/* anchor */".*", Pattern.CASE_INSENSITIVE);
/**
* Parses given URI-like string.
*/
public WebAddress(String address) throws IllegalArgumentException {
public WebAddress(@Nullable String address) throws IllegalArgumentException {
if (address == null) {
throw new IllegalArgumentException("address can't be null");
@@ -113,6 +114,7 @@ class WebAddress {
}
}
@NonNull
@Override
public String toString() {
@@ -8,7 +8,7 @@ import android.content.DialogInterface;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import java.util.Arrays;
@@ -16,9 +16,8 @@ import java.util.List;
import acr.browser.lightning.R;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.preference.PreferenceManager;
public class AdvancedSettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
public class AdvancedSettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
private static final String SETTINGS_NEWWINDOW = "allow_new_window";
private static final String SETTINGS_ENABLECOOKIES = "allow_cookies";
@@ -29,7 +28,6 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
private static final String SETTINGS_TEXTENCODING = "text_encoding";
private Activity mActivity;
private PreferenceManager mPreferences;
private CheckBoxPreference cbAllowPopups, cbenablecookies, cbcookiesInkognito, cbrestoreTabs;
private Preference renderingmode, urlcontent, textEncoding;
private CharSequence[] mUrlOptions;
@@ -46,8 +44,6 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
}
private void initPrefs() {
// mPreferences storage
mPreferences = PreferenceManager.getInstance();
renderingmode = findPreference(SETTINGS_RENDERINGMODE);
textEncoding = findPreference(SETTINGS_TEXTENCODING);
@@ -65,7 +61,7 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
cbcookiesInkognito.setOnPreferenceChangeListener(this);
cbrestoreTabs.setOnPreferenceChangeListener(this);
switch (mPreferences.getRenderingMode()) {
switch (mPreferenceManager.getRenderingMode()) {
case 0:
renderingmode.setSummary(getString(R.string.name_normal));
break;
@@ -78,22 +74,25 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
case 3:
renderingmode.setSummary(getString(R.string.name_inverted_grayscale));
break;
case 4:
renderingmode.setSummary(getString(R.string.name_increase_contrast));
break;
}
textEncoding.setSummary(mPreferences.getTextEncoding());
textEncoding.setSummary(mPreferenceManager.getTextEncoding());
mUrlOptions = getResources().getStringArray(R.array.url_content_array);
int option = mPreferences.getUrlBoxContentChoice();
int option = mPreferenceManager.getUrlBoxContentChoice();
urlcontent.setSummary(mUrlOptions[option]);
cbAllowPopups.setChecked(mPreferences.getPopupsEnabled());
cbenablecookies.setChecked(mPreferences.getCookiesEnabled());
cbcookiesInkognito.setChecked(mPreferences.getIncognitoCookiesEnabled());
cbrestoreTabs.setChecked(mPreferences.getRestoreLostTabsEnabled());
cbAllowPopups.setChecked(mPreferenceManager.getPopupsEnabled());
cbenablecookies.setChecked(mPreferenceManager.getCookiesEnabled());
cbcookiesInkognito.setChecked(mPreferenceManager.getIncognitoCookiesEnabled());
cbrestoreTabs.setChecked(mPreferenceManager.getRestoreLostTabsEnabled());
}
@Override
public boolean onPreferenceClick(Preference preference) {
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_RENDERINGMODE:
renderPicker();
@@ -110,23 +109,23 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
// switch preferences
switch (preference.getKey()) {
case SETTINGS_NEWWINDOW:
mPreferences.setPopupsEnabled((Boolean) newValue);
mPreferenceManager.setPopupsEnabled((Boolean) newValue);
cbAllowPopups.setChecked((Boolean) newValue);
return true;
case SETTINGS_ENABLECOOKIES:
mPreferences.setCookiesEnabled((Boolean) newValue);
mPreferenceManager.setCookiesEnabled((Boolean) newValue);
cbenablecookies.setChecked((Boolean) newValue);
return true;
case SETTINGS_COOKIESINKOGNITO:
mPreferences.setIncognitoCookiesEnabled((Boolean) newValue);
mPreferenceManager.setIncognitoCookiesEnabled((Boolean) newValue);
cbcookiesInkognito.setChecked((Boolean) newValue);
return true;
case SETTINGS_RESTORETABS:
mPreferences.setRestoreLostTabsEnabled((Boolean) newValue);
mPreferenceManager.setRestoreLostTabsEnabled((Boolean) newValue);
cbrestoreTabs.setChecked((Boolean) newValue);
return true;
default:
@@ -140,14 +139,15 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
CharSequence[] chars = {mActivity.getString(R.string.name_normal),
mActivity.getString(R.string.name_inverted),
mActivity.getString(R.string.name_grayscale),
mActivity.getString(R.string.name_inverted_grayscale)};
mActivity.getString(R.string.name_inverted_grayscale),
mActivity.getString(R.string.name_increase_contrast)};
int n = mPreferences.getRenderingMode();
int n = mPreferenceManager.getRenderingMode();
picker.setSingleChoiceItems(chars, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setRenderingMode(which);
mPreferenceManager.setRenderingMode(which);
switch (which) {
case 0:
renderingmode.setSummary(getString(R.string.name_normal));
@@ -161,6 +161,9 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
case 3:
renderingmode.setSummary(getString(R.string.name_inverted_grayscale));
break;
case 4:
renderingmode.setSummary(getString(R.string.name_increase_contrast));
break;
}
}
});
@@ -172,12 +175,12 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.text_encoding));
final List<String> textEncodingList = Arrays.asList(Constants.TEXT_ENCODINGS);
int n = textEncodingList.indexOf(mPreferences.getTextEncoding());
int n = textEncodingList.indexOf(mPreferenceManager.getTextEncoding());
picker.setSingleChoiceItems(Constants.TEXT_ENCODINGS, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setTextEncoding(Constants.TEXT_ENCODINGS[which]);
mPreferenceManager.setTextEncoding(Constants.TEXT_ENCODINGS[which]);
textEncoding.setSummary(Constants.TEXT_ENCODINGS[which]);
}
});
@@ -189,12 +192,12 @@ public class AdvancedSettingsFragment extends PreferenceFragment implements Pref
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.url_contents));
int n = mPreferences.getUrlBoxContentChoice();
int n = mPreferenceManager.getUrlBoxContentChoice();
picker.setSingleChoiceItems(mUrlOptions, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setUrlBoxContentChoice(which);
mPreferenceManager.setUrlBoxContentChoice(which);
if (which < mUrlOptions.length) {
urlcontent.setSummary(mUrlOptions[which]);
}
@@ -6,15 +6,26 @@ 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;
@@ -23,10 +34,14 @@ 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.utils.PermissionsManager;
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 {
@@ -34,13 +49,15 @@ public class BookmarkSettingsFragment extends PreferenceFragment implements Pref
private static final String SETTINGS_EXPORT = "export_bookmark";
private static final String SETTINGS_IMPORT = "import_bookmark";
private static final String SETTINGS_IMPORT_BROWSER = "import_browser";
private static final String SETTINGS_DELETE_BOOKMARKS = "delete_bookmarks";
private Activity mActivity;
@Inject
BookmarkManager mBookmarkManager;
@Nullable private Activity mActivity;
@Inject BookmarkManager mBookmarkManager;
private File[] mFileList;
private String[] mFileNameList;
private BookmarkLocalSync mSync;
@Nullable private BookmarkLocalSync mSync;
private static final String[] REQUIRED_PERMISSIONS = new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
@@ -48,16 +65,38 @@ public class BookmarkSettingsFragment extends PreferenceFragment implements Pref
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 = null;
if (mSync.isStockSupported()) {
list = mSync.getBookmarksFromStockBrowser();
} else if (mSync.isChromeSupported()) {
list = mSync.getBookmarksFromChrome();
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 != null && !list.isEmpty()) {
if (!list.isEmpty()) {
mBookmarkManager.addBookmarkList(list);
count = list.size();
}
@@ -67,14 +106,25 @@ public class BookmarkSettingsFragment extends PreferenceFragment implements Pref
@Override
protected void onPostExecute(Integer num) {
super.onPostExecute(num);
if (mActivity != null) {
Activity activity = mActivityReference.get();
if (activity != null) {
int number = num;
final String message = mActivity.getResources().getString(R.string.message_import);
Utils.showSnackbar(mActivity, number + " " + message);
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);
@@ -83,12 +133,13 @@ public class BookmarkSettingsFragment extends PreferenceFragment implements Pref
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.requestPermissionsIfNecessary(getActivity(), REQUIRED_PERMISSIONS);
permissionsManager.requestPermissionsIfNecessaryForResult(getActivity(), REQUIRED_PERMISSIONS, null);
}
}
@@ -100,49 +151,184 @@ public class BookmarkSettingsFragment extends PreferenceFragment implements Pref
private void initPrefs() {
Preference exportpref = findPreference(SETTINGS_EXPORT);
Preference importpref = findPreference(SETTINGS_IMPORT);
Preference exportPref = findPreference(SETTINGS_EXPORT);
Preference importPref = findPreference(SETTINGS_IMPORT);
Preference deletePref = findPreference(SETTINGS_DELETE_BOOKMARKS);
mSync = new BookmarkLocalSync(mActivity);
exportPref.setOnPreferenceClickListener(this);
importPref.setOnPreferenceClickListener(this);
deletePref.setOnPreferenceClickListener(this);
exportpref.setOnPreferenceClickListener(this);
importpref.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);
}
});
}
});
new Thread(mInitializeImportPreference).start();
}
private final Runnable mInitializeImportPreference = new Runnable() {
@Override
public void run() {
Preference importStock = findPreference(SETTINGS_IMPORT_BROWSER);
importStock.setEnabled(mSync.isStockSupported() || mSync.isChromeSupported());
importStock.setOnPreferenceClickListener(BookmarkSettingsFragment.this);
}
};
@Override
public boolean onPreferenceClick(Preference preference) {
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_EXPORT:
if (PermissionsManager.checkPermissions(getActivity(), REQUIRED_PERMISSIONS)) {
mBookmarkManager.exportBookmarks(getActivity());
}
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(getActivity(), REQUIRED_PERMISSIONS,
new PermissionsResultAction() {
@Override
public void onGranted() {
mBookmarkManager.exportBookmarks(getActivity());
}
@Override
public void onDenied(String permission) {
//TODO Show message
}
});
return true;
case SETTINGS_IMPORT:
if (PermissionsManager.checkPermissions(getActivity(), REQUIRED_PERMISSIONS)) {
loadFileList(null);
createDialog();
}
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(getActivity(), REQUIRED_PERMISSIONS,
new PermissionsResultAction() {
@Override
public void onGranted() {
loadFileList(null);
createDialog();
}
@Override
public void onDenied(String permission) {
//TODO Show message
}
});
return true;
case SETTINGS_IMPORT_BROWSER:
new ImportBookmarksTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
getSync().getSupportedBrowsers().subscribeOn(Schedulers.worker())
.observeOn(Schedulers.main()).subscribe(new OnSubscribe<List<Source>>() {
@Override
public void onNext(@Nullable List<Source> items) {
Activity activity = getActivity();
if (items == null || activity == null) {
return;
}
List<String> titles = buildTitleList(activity, items);
showChooserDialog(activity, titles);
}
});
return true;
case SETTINGS_DELETE_BOOKMARKS:
showDeleteBookmarksDialog();
return true;
default:
return false;
}
}
private void loadFileList(File path) {
private void showDeleteBookmarksDialog() {
Activity activity = getActivity();
if (activity == null) {
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.action_delete);
builder.setMessage(R.string.action_delete_all_bookmarks);
builder.setNegativeButton(R.string.no, null);
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mBookmarkManager.deleteAllBookmarks();
}
});
builder.show();
}
@NonNull
private List<String> buildTitleList(@NonNull Activity activity, @NonNull List<Source> items) {
List<String> titles = new ArrayList<>();
String title;
for (Source source : items) {
switch (source) {
case STOCK:
titles.add(getString(R.string.stock_browser));
break;
case CHROME_STABLE:
title = getTitle(activity, "com.android.chrome");
if (title != null) {
titles.add(title);
}
break;
case CHROME_BETA:
title = getTitle(activity, "com.chrome.beta");
if (title != null) {
titles.add(title);
}
break;
case CHROME_DEV:
title = getTitle(activity, "com.chrome.beta");
if (title != null) {
titles.add(title);
}
break;
default:
break;
}
}
return titles;
}
private void showChooserDialog(final Activity activity, List<String> list) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
final ArrayAdapter<String> adapter = new ArrayAdapter<>(activity,
android.R.layout.simple_list_item_1);
for (String title : list) {
adapter.add(title);
}
builder.setTitle(R.string.supported_browsers_title);
builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String title = adapter.getItem(which);
Source source = null;
if (title.equals(getString(R.string.stock_browser))) {
source = Source.STOCK;
} else if (title.equals(getTitle(activity, "com.android.chrome"))) {
source = Source.CHROME_STABLE;
} else if (title.equals(getTitle(activity, "com.android.beta"))) {
source = Source.CHROME_BETA;
} else if (title.equals(getTitle(activity, "com.android.dev"))) {
source = Source.CHROME_DEV;
}
if (source != null) {
new ImportBookmarksTask(activity, source).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
});
builder.show();
}
@Nullable
private static String getTitle(@NonNull Activity activity, @NonNull String packageName) {
PackageManager pm = activity.getPackageManager();
try {
ApplicationInfo info = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
CharSequence title = pm.getApplicationLabel(info);
if (title != null) {
return title.toString();
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
private void loadFileList(@Nullable File path) {
File file;
if (path != null) {
file = path;
@@ -175,7 +361,7 @@ public class BookmarkSettingsFragment extends PreferenceFragment implements Pref
private static class SortName implements Comparator<File> {
@Override
public int compare(File a, File b) {
public int compare(@NonNull File a, @NonNull File b) {
if (a.isDirectory() && b.isDirectory())
return a.getName().compareTo(b.getName());
@@ -2,6 +2,7 @@ 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;
@@ -30,40 +31,50 @@ import com.squareup.otto.Bus;
import com.squareup.otto.Subscribe;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.activity.BrowserActivity;
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.BookmarksDialogBuilder;
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;
/**
* Created by Stefano Pacifici on 25/08/15. Based on Anthony C. Restaino's code.
*/
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;
@Inject BookmarkManager mBookmarkManager;
// Event bus
@Inject
Bus mEventBus;
@Inject Bus mEventBus;
// Dialog builder
@Inject
BookmarksDialogBuilder mBookmarksDialogBuilder;
@Inject LightningDialogBuilder mBookmarksDialogBuilder;
@Inject PreferenceManager mPreferenceManager;
private TabsManager mTabsManager;
// Adapter
private BookmarkViewAdapter mBookmarkAdapter;
@@ -81,21 +92,36 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
// Colors
private int mIconColor, mScrollIndex;
// Init asynchronously the bookmark manager
private final Runnable mInitBookmarkManager = new Runnable() {
@Override
public void run() {
final Context context = getContext();
mBookmarkAdapter = new BookmarkViewAdapter(context, mBookmarks);
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false);
mBookmarksListView.setAdapter(mBookmarkAdapter);
}
};
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
@@ -107,7 +133,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
mScrollIndex = mBookmarksListView.getFirstVisiblePosition();
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(item.getTitle(), true), true);
} else {
mEventBus.post(new BookmarkEvents.Clicked(item));
mEventBus.post(new BrowserEvents.OpenUrlInCurrentTab(item.getUrl()));
}
}
};
@@ -116,7 +142,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
final HistoryItem item = mBookmarks.get(position);
handleLongPress(item, position);
handleLongPress(item);
return true;
}
};
@@ -124,17 +150,20 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
@Override
public void onResume() {
super.onResume();
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false);
if (mBookmarkAdapter != null) {
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false);
}
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
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() {
@@ -148,26 +177,20 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
}
});
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);
// Must be called here, only here we have a reference to the ListView
new Thread(mInitBookmarkManager).run();
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 onActivityCreated(@Nullable Bundle savedInstanceState) {
// TODO remove dependency on BrowserActivity
super.onActivityCreated(savedInstanceState);
final Activity activity = getActivity();
final PreferenceManager preferenceManager = PreferenceManager.getInstance();
boolean darkTheme = preferenceManager.getUseTheme() != 0 || ((BrowserActivity) activity).isIncognito();
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);
mBookmarkTitleImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN);
}
@Override
public void onStart() {
super.onStart();
@@ -180,21 +203,30 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
mEventBus.unregister(this);
}
@Subscribe
public void addBookmark(final BrowserEvents.AddBookmark event) {
final HistoryItem item = new HistoryItem(event.url, event.title);
if (mBookmarkManager.addBookmark(item)) {
mBookmarks.add(item);
Collections.sort(mBookmarks, new BookmarkManager.SortIgnoreCase());
mBookmarkAdapter.notifyDataSetChanged();
mEventBus.post(new BookmarkEvents.Added(item));
updateBookmarkIndicator(event.url);
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 currentPageInfo(final BrowserEvents.CurrentPageUrl event) {
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
@@ -224,7 +256,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
}
@Subscribe
public void bookmarkDeleted(final BookmarkEvents.Deleted event) {
public void bookmarkDeleted(@NonNull final BookmarkEvents.Deleted event) {
mBookmarks.remove(event.item);
if (event.item.isFolder()) {
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false);
@@ -233,7 +265,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
}
}
private void setBookmarkDataSet(List<HistoryItem> items, boolean animate) {
private void setBookmarkDataSet(@NonNull List<HistoryItem> items, boolean animate) {
mBookmarks.clear();
mBookmarks.addAll(items);
mBookmarkAdapter.notifyDataSetChanged();
@@ -291,19 +323,35 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
buttonImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN);
}
private void handleLongPress(final HistoryItem item, final int position) {
private void handleLongPress(@NonNull final HistoryItem item) {
if (item.isFolder()) {
mBookmarksDialogBuilder.showBookmarkFolderLongPressedDialog(getContext(), item);
} else {
mBookmarksDialogBuilder.showLongPressedDialogForUrl(getContext(), item);
mBookmarksDialogBuilder.showLongPressedDialogForBookmarkUrl(getContext(), item);
}
}
@Override
public void onClick(View v) {
public void onClick(@NonNull View v) {
switch (v.getId()) {
case R.id.action_add_bookmark:
mEventBus.post(new BookmarkEvents.WantToBookmarkCurrentPage());
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;
@@ -319,7 +367,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
final Context context;
public BookmarkViewAdapter(Context context, List<HistoryItem> data) {
public BookmarkViewAdapter(Context context, @NonNull List<HistoryItem> data) {
super(context, R.layout.bookmark_list_item, data);
this.context = context;
}
@@ -349,7 +397,7 @@ public class BookmarksFragment extends Fragment implements View.OnClickListener,
holder.favicon.setImageBitmap(mFolderBitmap);
} else if (web.getBitmap() == null) {
holder.favicon.setImageBitmap(mWebpageBitmap);
new ImageDownloadTask(holder.favicon, web, mWebpageBitmap)
new ImageDownloadTask(holder.favicon, web, mWebpageBitmap, context)
.executeOnExecutor(AsyncExecutor.getInstance());
} else {
holder.favicon.setImageBitmap(web.getBitmap());
@@ -0,0 +1,57 @@
package acr.browser.lightning.fragment;
import android.app.Activity;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.SwitchPreference;
import android.support.annotation.NonNull;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.Utils;
public class DebugSettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
private static final String LEAK_CANARY = "leak_canary_enabled";
@Inject PreferenceManager mPreferenceManager;
private SwitchPreference mSwitchLeakCanary;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
BrowserApp.getAppComponent().inject(this);
addPreferencesFromResource(R.xml.preference_debug);
mSwitchLeakCanary = (SwitchPreference) findPreference(LEAK_CANARY);
mSwitchLeakCanary.setChecked(mPreferenceManager.getUseLeakCanary());
mSwitchLeakCanary.setOnPreferenceChangeListener(this);
}
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
return false;
}
@Override
public boolean onPreferenceChange(@NonNull Preference preference, @NonNull Object newValue) {
switch (preference.getKey()) {
case LEAK_CANARY:
boolean value = Boolean.TRUE.equals(newValue);
mPreferenceManager.setUseLeakCanary(value);
Activity activity = getActivity();
if (activity != null) {
Utils.showSnackbar(activity, R.string.app_restart);
}
mSwitchLeakCanary.setChecked(value);
return true;
}
return false;
}
}
@@ -8,7 +8,7 @@ import android.content.DialogInterface;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.view.Gravity;
import android.view.LayoutInflater;
@@ -19,9 +19,8 @@ import android.widget.SeekBar;
import android.widget.TextView;
import acr.browser.lightning.R;
import acr.browser.lightning.preference.PreferenceManager;
public class DisplaySettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
public class DisplaySettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
private static final String SETTINGS_HIDESTATUSBAR = "fullScreenOption";
private static final String SETTINGS_FULLSCREEN = "fullscreen";
@@ -38,7 +37,6 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
private static final float XSMALL = 10.0f;
private Activity mActivity;
private PreferenceManager mPreferences;
private CheckBoxPreference cbstatus, cbfullscreen, cbviewport, cboverview, cbreflow;
private Preference theme;
private String[] mThemeOptions;
@@ -57,9 +55,8 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
private void initPrefs() {
// mPreferences storage
mPreferences = PreferenceManager.getInstance();
mThemeOptions = this.getResources().getStringArray(R.array.themes);
mCurrentTheme = mPreferences.getUseTheme();
mCurrentTheme = mPreferenceManager.getUseTheme();
theme = findPreference(SETTINGS_THEME);
Preference textsize = findPreference(SETTINGS_TEXTSIZE);
@@ -77,17 +74,17 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
cboverview.setOnPreferenceChangeListener(this);
cbreflow.setOnPreferenceChangeListener(this);
cbstatus.setChecked(mPreferences.getHideStatusBarEnabled());
cbfullscreen.setChecked(mPreferences.getFullScreenEnabled());
cbviewport.setChecked(mPreferences.getUseWideViewportEnabled());
cboverview.setChecked(mPreferences.getOverviewModeEnabled());
cbreflow.setChecked(mPreferences.getTextReflowEnabled());
cbstatus.setChecked(mPreferenceManager.getHideStatusBarEnabled());
cbfullscreen.setChecked(mPreferenceManager.getFullScreenEnabled());
cbviewport.setChecked(mPreferenceManager.getUseWideViewportEnabled());
cboverview.setChecked(mPreferenceManager.getOverviewModeEnabled());
cbreflow.setChecked(mPreferenceManager.getTextReflowEnabled());
theme.setSummary(mThemeOptions[mPreferences.getUseTheme()]);
theme.setSummary(mThemeOptions[mPreferenceManager.getUseTheme()]);
}
@Override
public boolean onPreferenceClick(Preference preference) {
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_THEME:
themePicker();
@@ -101,27 +98,27 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
// switch preferences
switch (preference.getKey()) {
case SETTINGS_HIDESTATUSBAR:
mPreferences.setHideStatusBarEnabled((Boolean) newValue);
mPreferenceManager.setHideStatusBarEnabled((Boolean) newValue);
cbstatus.setChecked((Boolean) newValue);
return true;
case SETTINGS_FULLSCREEN:
mPreferences.setFullScreenEnabled((Boolean) newValue);
mPreferenceManager.setFullScreenEnabled((Boolean) newValue);
cbfullscreen.setChecked((Boolean) newValue);
return true;
case SETTINGS_VIEWPORT:
mPreferences.setUseWideViewportEnabled((Boolean) newValue);
mPreferenceManager.setUseWideViewportEnabled((Boolean) newValue);
cbviewport.setChecked((Boolean) newValue);
return true;
case SETTINGS_OVERVIEWMODE:
mPreferences.setOverviewModeEnabled((Boolean) newValue);
mPreferenceManager.setOverviewModeEnabled((Boolean) newValue);
cboverview.setChecked((Boolean) newValue);
return true;
case SETTINGS_REFLOW:
mPreferences.setTextReflowEnabled((Boolean) newValue);
mPreferenceManager.setTextReflowEnabled((Boolean) newValue);
cbreflow.setChecked((Boolean) newValue);
return true;
default:
@@ -142,14 +139,14 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
bar.setOnSeekBarChangeListener(new TextSeekBarListener(sample));
final int MAX = 5;
bar.setMax(MAX);
bar.setProgress(MAX - mPreferences.getTextSize());
bar.setProgress(MAX - mPreferenceManager.getTextSize());
builder.setView(view);
builder.setTitle(R.string.title_text_size);
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
mPreferences.setTextSize(MAX - bar.getProgress());
mPreferenceManager.setTextSize(MAX - bar.getProgress());
}
});
@@ -179,12 +176,12 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.theme));
int n = mPreferences.getUseTheme();
int n = mPreferenceManager.getUseTheme();
picker.setSingleChoiceItems(mThemeOptions, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setUseTheme(which);
mPreferenceManager.setUseTheme(which);
if (which < mThemeOptions.length) {
theme.setSummary(mThemeOptions[which]);
}
@@ -195,7 +192,7 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
@Override
public void onClick(DialogInterface dialog, int which) {
if (mCurrentTheme != mPreferences.getUseTheme()) {
if (mCurrentTheme != mPreferenceManager.getUseTheme()) {
getActivity().onBackPressed();
}
}
@@ -203,7 +200,7 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe
picker.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
if (mCurrentTheme != mPreferences.getUseTheme()) {
if (mCurrentTheme != mPreferenceManager.getUseTheme()) {
getActivity().onBackPressed();
}
}
@@ -5,11 +5,12 @@ 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.preference.PreferenceFragment;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
@@ -23,12 +24,11 @@ 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.preference.PreferenceManager;
import acr.browser.lightning.utils.ProxyUtils;
import acr.browser.lightning.utils.ThemeUtils;
import acr.browser.lightning.utils.Utils;
public class GeneralSettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
public class GeneralSettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
private static final String SETTINGS_PROXY = "proxy";
private static final String SETTINGS_FLASH = "cb_flash";
@@ -45,13 +45,11 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
private Activity mActivity;
private static final int API = android.os.Build.VERSION.SDK_INT;
private PreferenceManager mPreferences;
private CharSequence[] mProxyChoices;
private Preference proxy, useragent, downloadloc, home, searchengine;
private String mDownloadLocation;
private int mAgentChoice;
private String mHomepage;
private CheckBoxPreference cbFlash, cbAds, cbImages, cbJsScript, cbColorMode, cbgooglesuggest, cbDrawerTabs;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -65,22 +63,19 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
}
private void initPrefs() {
// mPreferences storage
mPreferences = PreferenceManager.getInstance();
proxy = findPreference(SETTINGS_PROXY);
useragent = findPreference(SETTINGS_USERAGENT);
downloadloc = findPreference(SETTINGS_DOWNLOAD);
home = findPreference(SETTINGS_HOME);
searchengine = findPreference(SETTINGS_SEARCHENGINE);
cbFlash = (CheckBoxPreference) findPreference(SETTINGS_FLASH);
cbAds = (CheckBoxPreference) findPreference(SETTINGS_ADS);
cbImages = (CheckBoxPreference) findPreference(SETTINGS_IMAGES);
cbJsScript = (CheckBoxPreference) findPreference(SETTINGS_JAVASCRIPT);
cbColorMode = (CheckBoxPreference) findPreference(SETTINGS_COLORMODE);
cbgooglesuggest = (CheckBoxPreference) findPreference(SETTINGS_GOOGLESUGGESTIONS);
cbDrawerTabs = (CheckBoxPreference) findPreference(SETTINGS_DRAWERTABS);
CheckBoxPreference cbFlash = (CheckBoxPreference) findPreference(SETTINGS_FLASH);
CheckBoxPreference cbAds = (CheckBoxPreference) findPreference(SETTINGS_ADS);
CheckBoxPreference cbImages = (CheckBoxPreference) findPreference(SETTINGS_IMAGES);
CheckBoxPreference cbJsScript = (CheckBoxPreference) findPreference(SETTINGS_JAVASCRIPT);
CheckBoxPreference cbColorMode = (CheckBoxPreference) findPreference(SETTINGS_COLORMODE);
CheckBoxPreference cbgooglesuggest = (CheckBoxPreference) findPreference(SETTINGS_GOOGLESUGGESTIONS);
CheckBoxPreference cbDrawerTabs = (CheckBoxPreference) findPreference(SETTINGS_DRAWERTABS);
proxy.setOnPreferenceClickListener(this);
useragent.setOnPreferenceClickListener(this);
@@ -95,23 +90,23 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
cbgooglesuggest.setOnPreferenceChangeListener(this);
cbDrawerTabs.setOnPreferenceChangeListener(this);
mAgentChoice = mPreferences.getUserAgentChoice();
mHomepage = mPreferences.getHomepage();
mDownloadLocation = mPreferences.getDownloadDirectory();
mAgentChoice = mPreferenceManager.getUserAgentChoice();
mHomepage = mPreferenceManager.getHomepage();
mDownloadLocation = mPreferenceManager.getDownloadDirectory();
mProxyChoices = getResources().getStringArray(R.array.proxy_choices_array);
int choice = mPreferences.getProxyChoice();
int choice = mPreferenceManager.getProxyChoice();
if (choice == Constants.PROXY_MANUAL) {
proxy.setSummary(mPreferences.getProxyHost() + ':' + mPreferences.getProxyPort());
proxy.setSummary(mPreferenceManager.getProxyHost() + ':' + mPreferenceManager.getProxyPort());
} else {
proxy.setSummary(mProxyChoices[choice]);
}
if (API >= 19) {
mPreferences.setFlashSupport(0);
if (API >= Build.VERSION_CODES.KITKAT) {
mPreferenceManager.setFlashSupport(0);
}
setSearchEngineSummary(mPreferences.getSearchChoice());
setSearchEngineSummary(mPreferenceManager.getSearchChoice());
downloadloc.setSummary(mDownloadLocation);
@@ -139,27 +134,27 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
useragent.setSummary(getResources().getString(R.string.agent_custom));
}
int flashNum = mPreferences.getFlashSupport();
boolean imagesBool = mPreferences.getBlockImagesEnabled();
boolean enableJSBool = mPreferences.getJavaScriptEnabled();
int flashNum = mPreferenceManager.getFlashSupport();
boolean imagesBool = mPreferenceManager.getBlockImagesEnabled();
boolean enableJSBool = mPreferenceManager.getJavaScriptEnabled();
cbAds.setEnabled(Constants.FULL_VERSION);
cbFlash.setEnabled(API < 19);
cbFlash.setEnabled(API < Build.VERSION_CODES.KITKAT);
cbImages.setChecked(imagesBool);
cbJsScript.setChecked(enableJSBool);
cbFlash.setChecked(flashNum > 0);
cbAds.setChecked(Constants.FULL_VERSION && mPreferences.getAdBlockEnabled());
cbColorMode.setChecked(mPreferences.getColorModeEnabled());
cbgooglesuggest.setChecked(mPreferences.getGoogleSearchSuggestionsEnabled());
cbDrawerTabs.setChecked(mPreferences.getShowTabsInDrawer(true));
cbAds.setChecked(Constants.FULL_VERSION && mPreferenceManager.getAdBlockEnabled());
cbColorMode.setChecked(mPreferenceManager.getColorModeEnabled());
cbgooglesuggest.setChecked(mPreferenceManager.getGoogleSearchSuggestionsEnabled());
cbDrawerTabs.setChecked(mPreferenceManager.getShowTabsInDrawer(true));
}
private void searchUrlPicker() {
final AlertDialog.Builder urlPicker = new AlertDialog.Builder(mActivity);
urlPicker.setTitle(getResources().getString(R.string.custom_url));
final EditText getSearchUrl = new EditText(mActivity);
String mSearchUrl = mPreferences.getSearchUrl();
String mSearchUrl = mPreferenceManager.getSearchUrl();
getSearchUrl.setText(mSearchUrl);
urlPicker.setView(getSearchUrl);
urlPicker.setPositiveButton(getResources().getString(R.string.action_ok),
@@ -167,7 +162,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
@Override
public void onClick(DialogInterface dialog, int which) {
String text = getSearchUrl.getText().toString();
mPreferences.setSearchUrl(text);
mPreferenceManager.setSearchUrl(text);
searchengine.setSummary(getResources().getString(R.string.custom_url) + ": "
+ text);
}
@@ -184,7 +179,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
mPreferences.setFlashSupport(1);
mPreferenceManager.setFlashSupport(1);
}
})
.setNegativeButton(getResources().getString(R.string.action_auto),
@@ -192,13 +187,13 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setFlashSupport(2);
mPreferenceManager.setFlashSupport(2);
}
}).setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
mPreferences.setFlashSupport(0);
mPreferenceManager.setFlashSupport(0);
}
});
@@ -209,7 +204,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
private void proxyChoicePicker() {
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.http_proxy));
picker.setSingleChoiceItems(mProxyChoices, mPreferences.getProxyChoice(),
picker.setSingleChoiceItems(mProxyChoices, mPreferenceManager.getProxyChoice(),
new DialogInterface.OnClickListener() {
@Override
@@ -234,7 +229,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
break;
}
mPreferences.setProxyChoice(choice);
mPreferenceManager.setProxyChoice(choice);
if (choice < mProxyChoices.length)
proxy.setSummary(mProxyChoices[choice]);
}
@@ -252,8 +247,8 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
filterArray[0] = new InputFilter.LengthFilter(maxCharacters - 1);
eProxyPort.setFilters(filterArray);
eProxyHost.setText(mPreferences.getProxyHost());
eProxyPort.setText(Integer.toString(mPreferences.getProxyPort()));
eProxyHost.setText(mPreferenceManager.getProxyHost());
eProxyPort.setText(Integer.toString(mPreferenceManager.getProxyPort()));
new AlertDialog.Builder(mActivity)
.setTitle(R.string.manual_proxy)
@@ -268,10 +263,10 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
// larger than max integer
proxyPort = Integer.parseInt(eProxyPort.getText().toString());
} catch (NumberFormatException ignored) {
proxyPort = mPreferences.getProxyPort();
proxyPort = mPreferenceManager.getProxyPort();
}
mPreferences.setProxyHost(proxyHost);
mPreferences.setProxyPort(proxyPort);
mPreferenceManager.setProxyHost(proxyHost);
mPreferenceManager.setProxyPort(proxyPort);
proxy.setSummary(proxyHost + ':' + proxyPort);
}
}).show();
@@ -285,13 +280,13 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
"DuckDuckGo (Privacy)", "DuckDuckGo Lite (Privacy)", "Baidu (Chinese)",
"Yandex (Russian)"};
int n = mPreferences.getSearchChoice();
int n = mPreferenceManager.getSearchChoice();
picker.setSingleChoiceItems(chars, n, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setSearchChoice(which);
mPreferenceManager.setSearchChoice(which);
setSearchEngineSummary(which);
}
});
@@ -302,7 +297,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
private void homepageDialog() {
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.home));
mHomepage = mPreferences.getHomepage();
mHomepage = mPreferenceManager.getHomepage();
int n;
if (mHomepage.contains("about:home")) {
n = 1;
@@ -320,15 +315,15 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
public void onClick(DialogInterface dialog, int which) {
switch (which + 1) {
case 1:
mPreferences.setHomepage("about:home");
mPreferenceManager.setHomepage("about:home");
home.setSummary(getResources().getString(R.string.action_homepage));
break;
case 2:
mPreferences.setHomepage("about:blank");
mPreferenceManager.setHomepage("about:blank");
home.setSummary(getResources().getString(R.string.action_blank));
break;
case 3:
mPreferences.setHomepage("about:bookmarks");
mPreferenceManager.setHomepage("about:bookmarks");
home.setSummary(getResources().getString(R.string.action_bookmarks));
break;
case 4:
@@ -345,11 +340,12 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
final AlertDialog.Builder homePicker = new AlertDialog.Builder(mActivity);
homePicker.setTitle(getResources().getString(R.string.title_custom_homepage));
final EditText getHome = new EditText(mActivity);
mHomepage = mPreferences.getHomepage();
mHomepage = mPreferenceManager.getHomepage();
if (!mHomepage.startsWith("about:")) {
getHome.setText(mHomepage);
} else {
getHome.setText("http://www.google.com");
String defaultUrl = "https://www.google.com";
getHome.setText(defaultUrl);
}
homePicker.setView(getHome);
homePicker.setPositiveButton(getResources().getString(R.string.action_ok),
@@ -357,7 +353,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
@Override
public void onClick(DialogInterface dialog, int which) {
String text = getHome.getText().toString();
mPreferences.setHomepage(text);
mPreferenceManager.setHomepage(text);
home.setSummary(text);
}
});
@@ -367,7 +363,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
private void downloadLocDialog() {
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
picker.setTitle(getResources().getString(R.string.title_download_location));
mDownloadLocation = mPreferences.getDownloadDirectory();
mDownloadLocation = mPreferenceManager.getDownloadDirectory();
int n;
if (mDownloadLocation.contains(Environment.DIRECTORY_DOWNLOADS)) {
n = 0;
@@ -381,7 +377,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0:
mPreferences.setDownloadDirectory(DownloadHandler.DEFAULT_DOWNLOAD_PATH);
mPreferenceManager.setDownloadDirectory(DownloadHandler.DEFAULT_DOWNLOAD_PATH);
downloadloc.setSummary(DownloadHandler.DEFAULT_DOWNLOAD_PATH);
break;
case 1:
@@ -397,12 +393,12 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
private void agentDialog() {
AlertDialog.Builder agentPicker = new AlertDialog.Builder(mActivity);
agentPicker.setTitle(getResources().getString(R.string.title_user_agent));
mAgentChoice = mPreferences.getUserAgentChoice();
mAgentChoice = mPreferenceManager.getUserAgentChoice();
agentPicker.setSingleChoiceItems(R.array.user_agent, mAgentChoice - 1,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mPreferences.setUserAgentChoice(which + 1);
mPreferenceManager.setUserAgentChoice(which + 1);
switch (which + 1) {
case 1:
useragent.setSummary(getResources().getString(R.string.agent_default));
@@ -434,7 +430,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
@Override
public void onClick(DialogInterface dialog, int which) {
String text = getAgent.getText().toString();
mPreferences.setUserAgentString(text);
mPreferenceManager.setUserAgentString(text);
useragent.setSummary(getResources().getString(R.string.agent_custom));
}
});
@@ -448,12 +444,12 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
final EditText getDownload = new EditText(mActivity);
getDownload.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
getDownload.setText(PreferenceManager.getInstance().getDownloadDirectory());
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(mPreferences.getDownloadDirectory());
getDownload.setText(mPreferenceManager.getDownloadDirectory());
layout.addView(getDownload);
downLocationPicker.setView(layout);
@@ -463,7 +459,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
public void onClick(DialogInterface dialog, int which) {
String text = getDownload.getText().toString();
text = DownloadHandler.addNecessarySlashes(text);
mPreferences.setDownloadDirectory(text);
mPreferenceManager.setDownloadDirectory(text);
downloadloc.setSummary(text);
}
});
@@ -508,7 +504,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
}
@Override
public boolean onPreferenceClick(Preference preference) {
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_PROXY:
proxyChoicePicker();
@@ -531,45 +527,43 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
// switch preferences
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
boolean checked = false;
if (newValue instanceof Boolean) {
checked = (Boolean) newValue;
}
switch (preference.getKey()) {
case SETTINGS_FLASH:
if (cbFlash.isChecked()) {
getFlashChoice();
} else {
mPreferences.setFlashSupport(0);
}
if (!Utils.isFlashInstalled(mActivity) && cbFlash.isChecked()) {
if (!Utils.isFlashInstalled(mActivity) && checked) {
Utils.createInformativeDialog(mActivity, R.string.title_warning, R.string.dialog_adobe_not_installed);
cbFlash.setEnabled(false);
mPreferences.setFlashSupport(0);
mPreferenceManager.setFlashSupport(0);
return false;
} else {
if (checked) {
getFlashChoice();
} else {
mPreferenceManager.setFlashSupport(0);
}
}
cbFlash.setChecked((Boolean) newValue);
return true;
case SETTINGS_ADS:
mPreferences.setAdBlockEnabled((Boolean) newValue);
cbAds.setChecked((Boolean) newValue);
mPreferenceManager.setAdBlockEnabled(checked);
return true;
case SETTINGS_IMAGES:
mPreferences.setBlockImagesEnabled((Boolean) newValue);
cbImages.setChecked((Boolean) newValue);
mPreferenceManager.setBlockImagesEnabled(checked);
return true;
case SETTINGS_JAVASCRIPT:
mPreferences.setJavaScriptEnabled((Boolean) newValue);
cbJsScript.setChecked((Boolean) newValue);
mPreferenceManager.setJavaScriptEnabled(checked);
return true;
case SETTINGS_COLORMODE:
mPreferences.setColorModeEnabled((Boolean) newValue);
cbColorMode.setChecked((Boolean) newValue);
mPreferenceManager.setColorModeEnabled(checked);
return true;
case SETTINGS_GOOGLESUGGESTIONS:
mPreferences.setGoogleSearchSuggestionsEnabled((Boolean) newValue);
cbgooglesuggest.setChecked((Boolean) newValue);
mPreferenceManager.setGoogleSearchSuggestionsEnabled(checked);
return true;
case SETTINGS_DRAWERTABS:
mPreferences.setShowTabsInDrawer((Boolean) newValue);
cbDrawerTabs.setChecked((Boolean) newValue);
mPreferenceManager.setShowTabsInDrawer(checked);
return true;
default:
return false;
}
@@ -593,7 +587,7 @@ public class GeneralSettingsFragment extends PreferenceFragment implements Prefe
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
public void afterTextChanged(@NonNull Editable s) {
if (!DownloadHandler.isWriteAccessAvailable(s.toString())) {
this.getDownload.setTextColor(this.errorColor);
} else {
@@ -0,0 +1,27 @@
package acr.browser.lightning.fragment;
import android.os.Bundle;
import android.preference.PreferenceFragment;
import javax.inject.Inject;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.preference.PreferenceManager;
/**
* Simplify {@link PreferenceManager} inject in all the PreferenceFragments
*
* @author Stefano Pacifici
* @date 2015/09/16
*/
public class LightningPreferenceFragment extends PreferenceFragment {
@Inject
PreferenceManager mPreferenceManager;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
BrowserApp.getAppComponent().inject(this);
}
}
@@ -11,16 +11,20 @@ import android.os.Handler;
import android.os.Message;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.webkit.WebView;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.database.HistoryDatabase;
import acr.browser.lightning.utils.Utils;
import acr.browser.lightning.utils.WebUtils;
import acr.browser.lightning.view.LightningView;
public class PrivacySettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
public class PrivacySettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
private static final String SETTINGS_LOCATION = "location";
private static final String SETTINGS_THIRDPCOOKIES = "third_party";
@@ -33,16 +37,18 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
private static final String SETTINGS_CLEARCOOKIES = "clear_cookies";
private static final String SETTINGS_CLEARWEBSTORAGE = "clear_webstorage";
private static final String SETTINGS_WEBSTORAGEEXIT = "clear_webstorage_exit";
private static final String SETTINGS_DONOTTRACK = "do_not_track";
private static final String SETTINGS_IDENTIFYINGHEADERS = "remove_identifying_headers";
private Activity mActivity;
private PreferenceManager mPreferences;
private CheckBoxPreference cblocation, cb3cookies, cbsavepasswords, cbcacheexit, cbhistoryexit,
cbcookiesexit, cbwebstorageexit;
private Handler messageHandler;
private Handler mMessageHandler;
@Inject HistoryDatabase mHistoryDatabase;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
BrowserApp.getAppComponent().inject(this);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preference_privacy);
@@ -52,21 +58,20 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
}
private void initPrefs() {
// mPreferences storage
mPreferences = PreferenceManager.getInstance();
Preference clearcache = findPreference(SETTINGS_CLEARCACHE);
Preference clearhistory = findPreference(SETTINGS_CLEARHISTORY);
Preference clearcookies = findPreference(SETTINGS_CLEARCOOKIES);
Preference clearwebstorage = findPreference(SETTINGS_CLEARWEBSTORAGE);
cblocation = (CheckBoxPreference) findPreference(SETTINGS_LOCATION);
cb3cookies = (CheckBoxPreference) findPreference(SETTINGS_THIRDPCOOKIES);
cbsavepasswords = (CheckBoxPreference) findPreference(SETTINGS_SAVEPASSWORD);
cbcacheexit = (CheckBoxPreference) findPreference(SETTINGS_CACHEEXIT);
cbhistoryexit = (CheckBoxPreference) findPreference(SETTINGS_HISTORYEXIT);
cbcookiesexit = (CheckBoxPreference) findPreference(SETTINGS_COOKIEEXIT);
cbwebstorageexit = (CheckBoxPreference) findPreference(SETTINGS_WEBSTORAGEEXIT);
CheckBoxPreference cblocation = (CheckBoxPreference) findPreference(SETTINGS_LOCATION);
CheckBoxPreference cb3cookies = (CheckBoxPreference) findPreference(SETTINGS_THIRDPCOOKIES);
CheckBoxPreference cbsavepasswords = (CheckBoxPreference) findPreference(SETTINGS_SAVEPASSWORD);
CheckBoxPreference cbcacheexit = (CheckBoxPreference) findPreference(SETTINGS_CACHEEXIT);
CheckBoxPreference cbhistoryexit = (CheckBoxPreference) findPreference(SETTINGS_HISTORYEXIT);
CheckBoxPreference cbcookiesexit = (CheckBoxPreference) findPreference(SETTINGS_COOKIEEXIT);
CheckBoxPreference cbwebstorageexit = (CheckBoxPreference) findPreference(SETTINGS_WEBSTORAGEEXIT);
CheckBoxPreference cbDoNotTrack = (CheckBoxPreference) findPreference(SETTINGS_DONOTTRACK);
CheckBoxPreference cbIdentifyingHeaders = (CheckBoxPreference) findPreference(SETTINGS_IDENTIFYINGHEADERS);
clearcache.setOnPreferenceClickListener(this);
clearhistory.setOnPreferenceClickListener(this);
@@ -80,18 +85,28 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
cbhistoryexit.setOnPreferenceChangeListener(this);
cbcookiesexit.setOnPreferenceChangeListener(this);
cbwebstorageexit.setOnPreferenceChangeListener(this);
cbDoNotTrack.setOnPreferenceChangeListener(this);
cbIdentifyingHeaders.setOnPreferenceChangeListener(this);
cblocation.setChecked(mPreferences.getLocationEnabled());
cbsavepasswords.setChecked(mPreferences.getSavePasswordsEnabled());
cbcacheexit.setChecked(mPreferences.getClearCacheExit());
cbhistoryexit.setChecked(mPreferences.getClearHistoryExitEnabled());
cbcookiesexit.setChecked(mPreferences.getClearCookiesExitEnabled());
cb3cookies.setChecked(mPreferences.getBlockThirdPartyCookiesEnabled());
cbwebstorageexit.setChecked(mPreferences.getClearWebStorageExitEnabled());
cblocation.setChecked(mPreferenceManager.getLocationEnabled());
cbsavepasswords.setChecked(mPreferenceManager.getSavePasswordsEnabled());
cbcacheexit.setChecked(mPreferenceManager.getClearCacheExit());
cbhistoryexit.setChecked(mPreferenceManager.getClearHistoryExitEnabled());
cbcookiesexit.setChecked(mPreferenceManager.getClearCookiesExitEnabled());
cb3cookies.setChecked(mPreferenceManager.getBlockThirdPartyCookiesEnabled());
cbwebstorageexit.setChecked(mPreferenceManager.getClearWebStorageExitEnabled());
cbDoNotTrack.setChecked(mPreferenceManager.getDoNotTrackEnabled() && Utils.doesSupportHeaders());
cbIdentifyingHeaders.setChecked(mPreferenceManager.getRemoveIdentifyingHeadersEnabled() && Utils.doesSupportHeaders());
cbDoNotTrack.setEnabled(Utils.doesSupportHeaders());
cbIdentifyingHeaders.setEnabled(Utils.doesSupportHeaders());
String identifyingHeadersSummary = LightningView.HEADER_REQUESTED_WITH + ", " + LightningView.HEADER_WAP_PROFILE;
cbIdentifyingHeaders.setSummary(identifyingHeadersSummary);
cb3cookies.setEnabled(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP);
messageHandler = new MessageHandler(mActivity);
mMessageHandler = new MessageHandler(mActivity);
}
private static class MessageHandler extends Handler {
@@ -103,7 +118,7 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
}
@Override
public void handleMessage(Message msg) {
public void handleMessage(@NonNull Message msg) {
switch (msg.what) {
case 1:
Utils.showSnackbar(mHandlerContext, R.string.message_clear_history);
@@ -117,7 +132,7 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
}
@Override
public boolean onPreferenceClick(Preference preference) {
public boolean onPreferenceClick(@NonNull Preference preference) {
switch (preference.getKey()) {
case SETTINGS_CLEARCACHE:
clearCache();
@@ -144,13 +159,12 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
Thread clear = new Thread(new Runnable() {
BrowserApp.getIOThread().execute(new Runnable() {
@Override
public void run() {
clearHistory();
}
});
clear.start();
}
})
.setNegativeButton(getResources().getString(R.string.action_no), null).show();
@@ -164,13 +178,12 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
Thread clear = new Thread(new Runnable() {
BrowserApp.getTaskThread().execute(new Runnable() {
@Override
public void run() {
clearCookies();
}
});
clear.start();
}
})
.setNegativeButton(getResources().getString(R.string.action_no), null).show();
@@ -184,13 +197,13 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
}
private void clearHistory() {
WebUtils.clearHistory(getActivity());
messageHandler.sendEmptyMessage(1);
WebUtils.clearHistory(getActivity(), mHistoryDatabase);
mMessageHandler.sendEmptyMessage(1);
}
private void clearCookies() {
WebUtils.clearCookies(getActivity());
messageHandler.sendEmptyMessage(2);
mMessageHandler.sendEmptyMessage(2);
}
private void clearWebStorage() {
@@ -199,36 +212,34 @@ public class PrivacySettingsFragment extends PreferenceFragment implements Prefe
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
// switch preferences
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
switch (preference.getKey()) {
case SETTINGS_LOCATION:
mPreferences.setLocationEnabled((Boolean) newValue);
cblocation.setChecked((Boolean) newValue);
mPreferenceManager.setLocationEnabled((Boolean) newValue);
return true;
case SETTINGS_THIRDPCOOKIES:
mPreferences.setBlockThirdPartyCookiesEnabled((Boolean) newValue);
cb3cookies.setChecked((Boolean) newValue);
mPreferenceManager.setBlockThirdPartyCookiesEnabled((Boolean) newValue);
return true;
case SETTINGS_SAVEPASSWORD:
mPreferences.setSavePasswordsEnabled((Boolean) newValue);
cbsavepasswords.setChecked((Boolean) newValue);
mPreferenceManager.setSavePasswordsEnabled((Boolean) newValue);
return true;
case SETTINGS_CACHEEXIT:
mPreferences.setClearCacheExit((Boolean) newValue);
cbcacheexit.setChecked((Boolean) newValue);
mPreferenceManager.setClearCacheExit((Boolean) newValue);
return true;
case SETTINGS_HISTORYEXIT:
mPreferences.setClearHistoryExitEnabled((Boolean) newValue);
cbhistoryexit.setChecked((Boolean) newValue);
mPreferenceManager.setClearHistoryExitEnabled((Boolean) newValue);
return true;
case SETTINGS_COOKIEEXIT:
mPreferences.setClearCookiesExitEnabled((Boolean) newValue);
cbcookiesexit.setChecked((Boolean) newValue);
mPreferenceManager.setClearCookiesExitEnabled((Boolean) newValue);
return true;
case SETTINGS_WEBSTORAGEEXIT:
mPreferences.setClearWebStorageExitEnabled((Boolean) newValue);
cbwebstorageexit.setChecked((Boolean) newValue);
mPreferenceManager.setClearWebStorageExitEnabled((Boolean) newValue);
return true;
case SETTINGS_DONOTTRACK:
mPreferenceManager.setDoNotTrackEnabled((Boolean) newValue);
return true;
case SETTINGS_IDENTIFYINGHEADERS:
mPreferenceManager.setRemoveIdentifyingHeadersEnabled((Boolean) newValue);
return true;
default:
return false;
@@ -0,0 +1,413 @@
package acr.browser.lightning.fragment;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.TextViewCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.LayoutManager;
import android.support.v7.widget.SimpleItemAnimator;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.squareup.otto.Bus;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.activity.TabsManager;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.browser.TabsView;
import acr.browser.lightning.bus.NavigationEvents;
import acr.browser.lightning.bus.TabEvents;
import acr.browser.lightning.controller.UIController;
import acr.browser.lightning.fragment.anim.HorizontalItemAnimator;
import acr.browser.lightning.fragment.anim.VerticalItemAnimator;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.ThemeUtils;
import acr.browser.lightning.utils.Utils;
import acr.browser.lightning.view.LightningView;
/**
* A fragment that holds and manages the tabs and interaction with the tabs.
* It is reliant on the BrowserController in order to get the current UI state
* of the browser. It also uses the BrowserController to signal that the UI needs
* to change. This class contains the adapter used by both the drawer tabs and
* the desktop tabs. It delegates touch events for the tab UI appropriately.
*/
public class TabsFragment extends Fragment implements View.OnClickListener, View.OnLongClickListener, TabsView {
private static final String TAG = TabsFragment.class.getSimpleName();
/**
* Arguments boolean to tell the fragment it is displayed in the drawner or on the tab strip
* If true, the fragment is in the left drawner in the strip otherwise.
*/
public static final String VERTICAL_MODE = TAG + ".VERTICAL_MODE";
public static final String IS_INCOGNITO = TAG + ".IS_INCOGNITO";
private boolean mIsIncognito, mDarkTheme;
private int mIconColor;
private boolean mColorMode = true;
private boolean mShowInNavigationDrawer;
@Nullable private LightningViewAdapter mTabsAdapter;
private UIController mUiController;
private RecyclerView mRecyclerView;
private TabsManager mTabsManager;
@Inject Bus mBus;
@Inject PreferenceManager mPreferences;
public TabsFragment() {
BrowserApp.getAppComponent().inject(this);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Bundle arguments = getArguments();
final Context context = getContext();
mUiController = (UIController) getActivity();
mTabsManager = mUiController.getTabModel();
mIsIncognito = arguments.getBoolean(IS_INCOGNITO, false);
mShowInNavigationDrawer = arguments.getBoolean(VERTICAL_MODE, true);
mDarkTheme = mPreferences.getUseTheme() != 0 || mIsIncognito;
mColorMode = mPreferences.getColorModeEnabled();
mColorMode &= !mDarkTheme;
mIconColor = mDarkTheme ?
ThemeUtils.getIconDarkThemeColor(context) :
ThemeUtils.getIconLightThemeColor(context);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view;
final LayoutManager layoutManager;
if (mShowInNavigationDrawer) {
view = inflater.inflate(R.layout.tab_drawer, container, false);
layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
setupFrameLayoutButton(view, R.id.tab_header_button, R.id.plusIcon);
setupFrameLayoutButton(view, R.id.new_tab_button, R.id.icon_plus);
setupFrameLayoutButton(view, R.id.action_back, R.id.icon_back);
setupFrameLayoutButton(view, R.id.action_forward, R.id.icon_forward);
setupFrameLayoutButton(view, R.id.action_home, R.id.icon_home);
} else {
view = inflater.inflate(R.layout.tab_strip, container, false);
layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false);
ImageView newTab = (ImageView) view.findViewById(R.id.new_tab_button);
newTab.setColorFilter(ThemeUtils.getIconDarkThemeColor(getActivity()));
newTab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mUiController.newTabClicked();
}
});
}
mRecyclerView = (RecyclerView) view.findViewById(R.id.tabs_list);
SimpleItemAnimator animator;
if (mShowInNavigationDrawer) {
animator = new VerticalItemAnimator();
} else {
animator = new HorizontalItemAnimator();
}
animator.setSupportsChangeAnimations(false);
animator.setAddDuration(200);
animator.setChangeDuration(0);
animator.setRemoveDuration(200);
animator.setMoveDuration(200);
mRecyclerView.setLayerType(View.LAYER_TYPE_NONE, null);
mRecyclerView.setItemAnimator(animator);
mRecyclerView.setLayoutManager(layoutManager);
mTabsAdapter = new LightningViewAdapter(mShowInNavigationDrawer);
mRecyclerView.setAdapter(mTabsAdapter);
mRecyclerView.setHasFixedSize(true);
return view;
}
private void setupFrameLayoutButton(@NonNull final View root, @IdRes final int buttonId,
@IdRes final int imageId) {
final View frameButton = root.findViewById(buttonId);
final ImageView buttonImage = (ImageView) root.findViewById(imageId);
frameButton.setOnClickListener(this);
frameButton.setOnLongClickListener(this);
buttonImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN);
}
@Override
public void onDestroyView() {
super.onDestroyView();
mTabsAdapter = null;
}
@Override
public void onStart() {
super.onStart();
mBus.register(this);
}
@Override
public void onResume() {
super.onResume();
// Force adapter refresh
if (mTabsAdapter != null) {
mTabsAdapter.notifyDataSetChanged();
}
}
@Override
public void onStop() {
super.onStop();
mBus.unregister(this);
}
public void reinitializePreferences() {
Activity activity = getActivity();
if (activity == null) {
return;
}
mDarkTheme = mPreferences.getUseTheme() != 0 || mIsIncognito;
mColorMode = mPreferences.getColorModeEnabled();
mColorMode &= !mDarkTheme;
mIconColor = mDarkTheme ?
ThemeUtils.getIconDarkThemeColor(activity) :
ThemeUtils.getIconLightThemeColor(activity);
if (mTabsAdapter != null) {
mTabsAdapter.notifyDataSetChanged();
}
}
@Override
public void onClick(@NonNull View v) {
switch (v.getId()) {
case R.id.tab_header_button:
mUiController.showCloseDialog(mTabsManager.indexOfCurrentTab());
break;
case R.id.new_tab_button:
mBus.post(new TabEvents.NewTab());
break;
case R.id.action_back:
mBus.post(new NavigationEvents.GoBack());
break;
case R.id.action_forward:
mBus.post(new NavigationEvents.GoForward());
break;
case R.id.action_home:
mBus.post(new NavigationEvents.GoHome());
default:
break;
}
}
@Override
public boolean onLongClick(@NonNull View v) {
switch (v.getId()) {
case R.id.action_new_tab:
mBus.post(new TabEvents.NewTabLongPress());
break;
default:
break;
}
return true;
}
@Override
public void tabAdded() {
if (mTabsAdapter != null) {
mTabsAdapter.notifyItemInserted(mTabsManager.last());
mRecyclerView.postDelayed(new Runnable() {
@Override
public void run() {
mRecyclerView.smoothScrollToPosition(mTabsAdapter.getItemCount() - 1);
}
}, 500);
}
}
@Override
public void tabRemoved(int position) {
if (mTabsAdapter != null) {
mTabsAdapter.notifyItemRemoved(position);
}
}
@Override
public void tabChanged(int position) {
if (mTabsAdapter != null) {
mTabsAdapter.notifyItemChanged(position);
}
}
private class LightningViewAdapter extends RecyclerView.Adapter<LightningViewAdapter.LightningViewHolder> {
private final int mLayoutResourceId;
@Nullable private final Drawable mBackgroundTabDrawable;
@Nullable private final Drawable mForegroundTabDrawable;
@Nullable private final Bitmap mForegroundTabBitmap;
private ColorMatrix mColorMatrix;
private Paint mPaint;
private ColorFilter mFilter;
private static final float DESATURATED = 0.5f;
private final boolean mDrawerTabs;
public LightningViewAdapter(final boolean vertical) {
this.mLayoutResourceId = vertical ? R.layout.tab_list_item : R.layout.tab_list_item_horizontal;
this.mDrawerTabs = vertical;
if (vertical) {
mBackgroundTabDrawable = null;
mForegroundTabBitmap = null;
mForegroundTabDrawable = ThemeUtils.getSelectedBackground(getContext(), mDarkTheme);
} else {
int backgroundColor = Utils.mixTwoColors(ThemeUtils.getPrimaryColor(getContext()), Color.BLACK, 0.75f);
Bitmap backgroundTabBitmap = Bitmap.createBitmap(Utils.dpToPx(175), Utils.dpToPx(30), Bitmap.Config.ARGB_8888);
Utils.drawTrapezoid(new Canvas(backgroundTabBitmap), backgroundColor, true);
mBackgroundTabDrawable = new BitmapDrawable(getResources(), backgroundTabBitmap);
int foregroundColor = ThemeUtils.getPrimaryColor(getContext());
mForegroundTabBitmap = Bitmap.createBitmap(Utils.dpToPx(175), Utils.dpToPx(30), Bitmap.Config.ARGB_8888);
Utils.drawTrapezoid(new Canvas(mForegroundTabBitmap), foregroundColor, false);
mForegroundTabDrawable = null;
}
}
@NonNull
@Override
public LightningViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
View view = inflater.inflate(mLayoutResourceId, viewGroup, false);
return new LightningViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final LightningViewHolder holder, int position) {
holder.exitButton.setTag(position);
ViewCompat.jumpDrawablesToCurrentState(holder.exitButton);
LightningView web = mTabsManager.getTabAtPosition(position);
if (web == null) {
return;
}
holder.txtTitle.setText(web.getTitle());
final Bitmap favicon = web.getFavicon();
if (web.isForegroundTab()) {
TextViewCompat.setTextAppearance(holder.txtTitle, R.style.boldText);
Drawable foregroundDrawable;
if (!mDrawerTabs) {
foregroundDrawable = new BitmapDrawable(getResources(), mForegroundTabBitmap);
if (!mIsIncognito && mColorMode) {
foregroundDrawable.setColorFilter(mUiController.getUiColor(), PorterDuff.Mode.SRC_IN);
}
} else {
foregroundDrawable = mForegroundTabDrawable;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
holder.layout.setBackground(foregroundDrawable);
} else {
holder.layout.setBackgroundDrawable(foregroundDrawable);
}
if (!mIsIncognito && mColorMode) {
mUiController.changeToolbarBackground(favicon, foregroundDrawable);
}
holder.favicon.setImageBitmap(favicon);
} else {
TextViewCompat.setTextAppearance(holder.txtTitle, R.style.normalText);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
holder.layout.setBackground(mBackgroundTabDrawable);
} else {
holder.layout.setBackgroundDrawable(mBackgroundTabDrawable);
}
holder.favicon.setImageBitmap(getDesaturatedBitmap(favicon));
}
}
@Override
public int getItemCount() {
return mTabsManager.size();
}
public Bitmap getDesaturatedBitmap(@NonNull Bitmap favicon) {
Bitmap grayscaleBitmap = Bitmap.createBitmap(favicon.getWidth(),
favicon.getHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(grayscaleBitmap);
if (mColorMatrix == null || mFilter == null || mPaint == null) {
mPaint = new Paint();
mColorMatrix = new ColorMatrix();
mColorMatrix.setSaturation(DESATURATED);
mFilter = new ColorMatrixColorFilter(mColorMatrix);
mPaint.setColorFilter(mFilter);
}
c.drawBitmap(favicon, 0, 0, mPaint);
return grayscaleBitmap;
}
public class LightningViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
public LightningViewHolder(@NonNull View view) {
super(view);
txtTitle = (TextView) view.findViewById(R.id.textTab);
favicon = (ImageView) view.findViewById(R.id.faviconTab);
exit = (ImageView) view.findViewById(R.id.deleteButton);
layout = (LinearLayout) view.findViewById(R.id.tab_item_background);
exitButton = (FrameLayout) view.findViewById(R.id.deleteAction);
exit.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN);
exitButton.setOnClickListener(this);
layout.setOnClickListener(this);
layout.setOnLongClickListener(this);
}
@NonNull final TextView txtTitle;
@NonNull final ImageView favicon;
@NonNull final ImageView exit;
@NonNull final FrameLayout exitButton;
@NonNull final LinearLayout layout;
@Override
public void onClick(View v) {
if (v == exitButton) {
// Close tab
mBus.post(new TabEvents.CloseTab(getAdapterPosition()));
}
if (v == layout) {
mBus.post(new TabEvents.ShowTab(getAdapterPosition()));
}
}
@Override
public boolean onLongClick(View v) {
// Show close dialog
mBus.post(new TabEvents.ShowCloseDialog(getAdapterPosition()));
return true;
}
}
}
}
@@ -0,0 +1,675 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package acr.browser.lightning.fragment.anim;
import android.support.v4.animation.AnimatorCompatHelper;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPropertyAnimatorCompat;
import android.support.v4.view.ViewPropertyAnimatorListener;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.SimpleItemAnimator;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import java.util.ArrayList;
import java.util.List;
/**
* This implementation of {@link RecyclerView.ItemAnimator} provides basic
* animations on remove, add, and move events that happen to the items in
* a RecyclerView. RecyclerView uses a HorizontalItemAnimator by default.
*
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
*/
public class HorizontalItemAnimator extends SimpleItemAnimator {
private static final boolean DEBUG = false;
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
private ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
private ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
private ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
private static class MoveInfo {
public ViewHolder holder;
public int fromX, fromY, toX, toY;
private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
this.holder = holder;
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
}
private static class ChangeInfo {
public ViewHolder oldHolder, newHolder;
public int fromX, fromY, toX, toY;
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
this.oldHolder = oldHolder;
this.newHolder = newHolder;
}
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
this(oldHolder, newHolder);
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
@Override
public String toString() {
return "ChangeInfo{" +
"oldHolder=" + oldHolder +
", newHolder=" + newHolder +
", fromX=" + fromX +
", fromY=" + fromY +
", toX=" + toX +
", toY=" + toY +
'}';
}
}
@Override
public void runPendingAnimations() {
boolean removalsPending = !mPendingRemovals.isEmpty();
boolean movesPending = !mPendingMoves.isEmpty();
boolean changesPending = !mPendingChanges.isEmpty();
boolean additionsPending = !mPendingAdditions.isEmpty();
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
// nothing to animate
return;
}
// First, remove stuff
for (ViewHolder holder : mPendingRemovals) {
animateRemoveImpl(holder);
}
mPendingRemovals.clear();
// Next, move stuff
if (movesPending) {
final ArrayList<MoveInfo> moves = new ArrayList<>();
moves.addAll(mPendingMoves);
mMovesList.add(moves);
mPendingMoves.clear();
Runnable mover = new Runnable() {
@Override
public void run() {
for (MoveInfo moveInfo : moves) {
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
moveInfo.toX, moveInfo.toY);
}
moves.clear();
mMovesList.remove(moves);
}
};
if (removalsPending) {
View view = moves.get(0).holder.itemView;
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
} else {
mover.run();
}
}
// Next, change stuff, to run in parallel with move animations
if (changesPending) {
final ArrayList<ChangeInfo> changes = new ArrayList<>();
changes.addAll(mPendingChanges);
mChangesList.add(changes);
mPendingChanges.clear();
Runnable changer = new Runnable() {
@Override
public void run() {
for (ChangeInfo change : changes) {
animateChangeImpl(change);
}
changes.clear();
mChangesList.remove(changes);
}
};
if (removalsPending) {
ViewHolder holder = changes.get(0).oldHolder;
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
} else {
changer.run();
}
}
// Next, add stuff
if (additionsPending) {
final ArrayList<ViewHolder> additions = new ArrayList<>();
additions.addAll(mPendingAdditions);
mAdditionsList.add(additions);
mPendingAdditions.clear();
Runnable adder = new Runnable() {
public void run() {
for (ViewHolder holder : additions) {
animateAddImpl(holder);
}
additions.clear();
mAdditionsList.remove(additions);
}
};
if (removalsPending || movesPending || changesPending) {
long removeDuration = removalsPending ? getRemoveDuration() : 0;
long moveDuration = movesPending ? getMoveDuration() : 0;
long changeDuration = changesPending ? getChangeDuration() : 0;
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
View view = additions.get(0).itemView;
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
} else {
adder.run();
}
}
}
@Override
public boolean animateRemove(final ViewHolder holder) {
resetAnimation(holder);
mPendingRemovals.add(holder);
return true;
}
private void animateRemoveImpl(final ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mRemoveAnimations.add(holder);
animation.setDuration(getRemoveDuration())
.alpha(0).translationY(holder.itemView.getHeight())
.setInterpolator(new AccelerateInterpolator()).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchRemoveStarting(holder);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationY(view, 0);
dispatchRemoveFinished(holder);
mRemoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateAdd(final ViewHolder holder) {
resetAnimation(holder);
ViewCompat.setAlpha(holder.itemView, 0);
ViewCompat.setTranslationY(holder.itemView, holder.itemView.getHeight());
mPendingAdditions.add(holder);
return true;
}
private void animateAddImpl(final ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mAddAnimations.add(holder);
animation.alpha(1).translationY(0)
.setInterpolator(new DecelerateInterpolator()).setDuration(getAddDuration())
.setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchAddStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setAlpha(view, 1);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchAddFinished(holder);
mAddAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
int toX, int toY) {
final View view = holder.itemView;
fromX += ViewCompat.getTranslationX(holder.itemView);
fromY += ViewCompat.getTranslationY(holder.itemView);
int deltaX = toX - fromX;
int deltaY = toY - fromY;
if (deltaX == 0 && deltaY == 0) {
dispatchMoveFinished(holder);
return false;
}
resetAnimation(holder);
if (deltaX != 0) {
ViewCompat.setTranslationX(view, -deltaX);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, -deltaY);
}
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
return true;
}
private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
final View view = holder.itemView;
final int deltaX = toX - fromX;
final int deltaY = toY - fromY;
if (deltaX != 0) {
ViewCompat.animate(view).translationX(0);
}
if (deltaY != 0) {
ViewCompat.animate(view).translationY(0);
}
// TODO: make EndActions end listeners instead, since end actions aren't called when
// vpas are canceled (and can't end them. why?)
// need listener functionality in VPACompat for this. Ick.
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mMoveAnimations.add(holder);
animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchMoveStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
if (deltaX != 0) {
ViewCompat.setTranslationX(view, 0);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, 0);
}
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchMoveFinished(holder);
mMoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
// if (oldHolder != newHolder) {
// if (oldHolder != null) {
// dispatchChangeFinished(oldHolder, true);
// }
// if (newHolder != null) {
// dispatchChangeFinished(newHolder, false);
// }
// } else if (oldHolder != null) {
// dispatchChangeFinished(oldHolder, true);
// }
// return false;
if (oldHolder == newHolder) {
// Don't know how to run change animations when the same view holder is re-used.
// run a move animation to handle position changes.
if ((fromX - toX) == 0 && (fromY - toY) == 0) {
dispatchMoveFinished(oldHolder);
return false;
}
return animateMove(oldHolder, fromX, fromY, toX, toY);
}
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
resetAnimation(oldHolder);
int deltaX = (int) (toX - fromX - prevTranslationX);
int deltaY = (int) (toY - fromY - prevTranslationY);
// recover prev translation state after ending animation
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
if (newHolder != null) {
// carry over translation values
resetAnimation(newHolder);
ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
ViewCompat.setAlpha(newHolder.itemView, 0);
}
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
return true;
}
private void animateChangeImpl(final ChangeInfo changeInfo) {
final ViewHolder holder = changeInfo.oldHolder;
final View view = holder == null ? null : holder.itemView;
final ViewHolder newHolder = changeInfo.newHolder;
final View newView = newHolder != null ? newHolder.itemView : null;
if (view != null) {
final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
getChangeDuration());
mChangeAnimations.add(changeInfo.oldHolder);
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.oldHolder, true);
}
@Override
public void onAnimationEnd(View view) {
oldViewAnim.setListener(null);
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationX(view, 0);
ViewCompat.setTranslationY(view, 0);
dispatchChangeFinished(changeInfo.oldHolder, true);
mChangeAnimations.remove(changeInfo.oldHolder);
dispatchFinishedWhenDone();
}
}).start();
}
if (newView != null) {
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
mChangeAnimations.add(changeInfo.newHolder);
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
alpha(1).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.newHolder, false);
}
@Override
public void onAnimationEnd(View view) {
newViewAnimation.setListener(null);
ViewCompat.setAlpha(newView, 1);
ViewCompat.setTranslationX(newView, 0);
ViewCompat.setTranslationY(newView, 0);
dispatchChangeFinished(changeInfo.newHolder, false);
mChangeAnimations.remove(changeInfo.newHolder);
dispatchFinishedWhenDone();
}
}).start();
}
}
private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
for (int i = infoList.size() - 1; i >= 0; i--) {
ChangeInfo changeInfo = infoList.get(i);
if (endChangeAnimationIfNecessary(changeInfo, item)) {
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
infoList.remove(changeInfo);
}
}
}
}
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
if (changeInfo.oldHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
}
if (changeInfo.newHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
}
}
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
boolean oldItem = false;
if (changeInfo.newHolder == item) {
changeInfo.newHolder = null;
} else if (changeInfo.oldHolder == item) {
changeInfo.oldHolder = null;
oldItem = true;
} else {
return false;
}
ViewCompat.setAlpha(item.itemView, 1);
ViewCompat.setTranslationX(item.itemView, 0);
ViewCompat.setTranslationY(item.itemView, 0);
dispatchChangeFinished(item, oldItem);
return true;
}
@Override
public void endAnimation(ViewHolder item) {
final View view = item.itemView;
// this will trigger end callback which should set properties to their target values.
ViewCompat.animate(view).cancel();
// TODO if some other animations are chained to end, how do we cancel them as well?
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
MoveInfo moveInfo = mPendingMoves.get(i);
if (moveInfo.holder == item) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item);
mPendingMoves.remove(i);
}
}
endChangeAnimation(mPendingChanges, item);
if (mPendingRemovals.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchRemoveFinished(item);
}
if (mPendingAdditions.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
}
for (int i = mChangesList.size() - 1; i >= 0; i--) {
ArrayList<ChangeInfo> changes = mChangesList.get(i);
endChangeAnimation(changes, item);
if (changes.isEmpty()) {
mChangesList.remove(i);
}
}
for (int i = mMovesList.size() - 1; i >= 0; i--) {
ArrayList<MoveInfo> moves = mMovesList.get(i);
for (int j = moves.size() - 1; j >= 0; j--) {
MoveInfo moveInfo = moves.get(j);
if (moveInfo.holder == item) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item);
moves.remove(j);
if (moves.isEmpty()) {
mMovesList.remove(i);
}
break;
}
}
}
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
if (additions.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
if (additions.isEmpty()) {
mAdditionsList.remove(i);
}
}
}
// animations should be ended by the cancel above.
//noinspection PointlessBooleanExpression,ConstantConditions
if (mRemoveAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mRemoveAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mAddAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mAddAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mChangeAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mChangeAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mMoveAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mMoveAnimations list");
}
dispatchFinishedWhenDone();
}
private void resetAnimation(ViewHolder holder) {
AnimatorCompatHelper.clearInterpolator(holder.itemView);
endAnimation(holder);
}
@Override
public boolean isRunning() {
return (!mPendingAdditions.isEmpty() ||
!mPendingChanges.isEmpty() ||
!mPendingMoves.isEmpty() ||
!mPendingRemovals.isEmpty() ||
!mMoveAnimations.isEmpty() ||
!mRemoveAnimations.isEmpty() ||
!mAddAnimations.isEmpty() ||
!mChangeAnimations.isEmpty() ||
!mMovesList.isEmpty() ||
!mAdditionsList.isEmpty() ||
!mChangesList.isEmpty());
}
/**
* Check the state of currently pending and running animations. If there are none
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
* listeners.
*/
private void dispatchFinishedWhenDone() {
if (!isRunning()) {
dispatchAnimationsFinished();
}
}
@Override
public void endAnimations() {
int count = mPendingMoves.size();
for (int i = count - 1; i >= 0; i--) {
MoveInfo item = mPendingMoves.get(i);
View view = item.holder.itemView;
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item.holder);
mPendingMoves.remove(i);
}
count = mPendingRemovals.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mPendingRemovals.get(i);
dispatchRemoveFinished(item);
mPendingRemovals.remove(i);
}
count = mPendingAdditions.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mPendingAdditions.get(i);
View view = item.itemView;
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
mPendingAdditions.remove(i);
}
count = mPendingChanges.size();
for (int i = count - 1; i >= 0; i--) {
endChangeAnimationIfNecessary(mPendingChanges.get(i));
}
mPendingChanges.clear();
if (!isRunning()) {
return;
}
int listCount = mMovesList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<MoveInfo> moves = mMovesList.get(i);
count = moves.size();
for (int j = count - 1; j >= 0; j--) {
MoveInfo moveInfo = moves.get(j);
ViewHolder item = moveInfo.holder;
View view = item.itemView;
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(moveInfo.holder);
moves.remove(j);
if (moves.isEmpty()) {
mMovesList.remove(moves);
}
}
}
listCount = mAdditionsList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
count = additions.size();
for (int j = count - 1; j >= 0; j--) {
ViewHolder item = additions.get(j);
View view = item.itemView;
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
additions.remove(j);
if (additions.isEmpty()) {
mAdditionsList.remove(additions);
}
}
}
listCount = mChangesList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<ChangeInfo> changes = mChangesList.get(i);
count = changes.size();
for (int j = count - 1; j >= 0; j--) {
endChangeAnimationIfNecessary(changes.get(j));
if (changes.isEmpty()) {
mChangesList.remove(changes);
}
}
}
cancelAll(mRemoveAnimations);
cancelAll(mMoveAnimations);
cancelAll(mAddAnimations);
cancelAll(mChangeAnimations);
dispatchAnimationsFinished();
}
static void cancelAll(List<ViewHolder> viewHolders) {
for (int i = viewHolders.size() - 1; i >= 0; i--) {
ViewCompat.animate(viewHolders.get(i).itemView).cancel();
}
}
private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
@Override
public void onAnimationStart(View view) {}
@Override
public void onAnimationEnd(View view) {}
@Override
public void onAnimationCancel(View view) {}
}
}
@@ -0,0 +1,674 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package acr.browser.lightning.fragment.anim;
import android.support.v4.animation.AnimatorCompatHelper;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPropertyAnimatorCompat;
import android.support.v4.view.ViewPropertyAnimatorListener;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.SimpleItemAnimator;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import java.util.ArrayList;
import java.util.List;
/**
* This implementation of {@link RecyclerView.ItemAnimator} provides basic
* animations on remove, add, and move events that happen to the items in
* a RecyclerView. RecyclerView uses a VerticalItemAnimator by default.
*
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
*/
public class VerticalItemAnimator extends SimpleItemAnimator {
private static final boolean DEBUG = false;
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
private ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
private ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
private ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
private static class MoveInfo {
public ViewHolder holder;
public int fromX, fromY, toX, toY;
private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
this.holder = holder;
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
}
private static class ChangeInfo {
public ViewHolder oldHolder, newHolder;
public int fromX, fromY, toX, toY;
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
this.oldHolder = oldHolder;
this.newHolder = newHolder;
}
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
this(oldHolder, newHolder);
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
@Override
public String toString() {
return "ChangeInfo{" +
"oldHolder=" + oldHolder +
", newHolder=" + newHolder +
", fromX=" + fromX +
", fromY=" + fromY +
", toX=" + toX +
", toY=" + toY +
'}';
}
}
@Override
public void runPendingAnimations() {
boolean removalsPending = !mPendingRemovals.isEmpty();
boolean movesPending = !mPendingMoves.isEmpty();
boolean changesPending = !mPendingChanges.isEmpty();
boolean additionsPending = !mPendingAdditions.isEmpty();
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
// nothing to animate
return;
}
// First, remove stuff
for (ViewHolder holder : mPendingRemovals) {
animateRemoveImpl(holder);
}
mPendingRemovals.clear();
// Next, move stuff
if (movesPending) {
final ArrayList<MoveInfo> moves = new ArrayList<>();
moves.addAll(mPendingMoves);
mMovesList.add(moves);
mPendingMoves.clear();
Runnable mover = new Runnable() {
@Override
public void run() {
for (MoveInfo moveInfo : moves) {
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
moveInfo.toX, moveInfo.toY);
}
moves.clear();
mMovesList.remove(moves);
}
};
if (removalsPending) {
View view = moves.get(0).holder.itemView;
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
} else {
mover.run();
}
}
// Next, change stuff, to run in parallel with move animations
if (changesPending) {
final ArrayList<ChangeInfo> changes = new ArrayList<>();
changes.addAll(mPendingChanges);
mChangesList.add(changes);
mPendingChanges.clear();
Runnable changer = new Runnable() {
@Override
public void run() {
for (ChangeInfo change : changes) {
animateChangeImpl(change);
}
changes.clear();
mChangesList.remove(changes);
}
};
if (removalsPending) {
ViewHolder holder = changes.get(0).oldHolder;
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
} else {
changer.run();
}
}
// Next, add stuff
if (additionsPending) {
final ArrayList<ViewHolder> additions = new ArrayList<>();
additions.addAll(mPendingAdditions);
mAdditionsList.add(additions);
mPendingAdditions.clear();
Runnable adder = new Runnable() {
public void run() {
for (ViewHolder holder : additions) {
animateAddImpl(holder);
}
additions.clear();
mAdditionsList.remove(additions);
}
};
if (removalsPending || movesPending || changesPending) {
long removeDuration = removalsPending ? getRemoveDuration() : 0;
long moveDuration = movesPending ? getMoveDuration() : 0;
long changeDuration = changesPending ? getChangeDuration() : 0;
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
View view = additions.get(0).itemView;
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
} else {
adder.run();
}
}
}
@Override
public boolean animateRemove(final ViewHolder holder) {
resetAnimation(holder);
mPendingRemovals.add(holder);
return true;
}
private void animateRemoveImpl(final ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mRemoveAnimations.add(holder);
animation.setDuration(getRemoveDuration())
.alpha(0).translationX(-holder.itemView.getWidth() / 2)
.setInterpolator(new AccelerateInterpolator()).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchRemoveStarting(holder);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationX(view, 0);
dispatchRemoveFinished(holder);
mRemoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateAdd(final ViewHolder holder) {
resetAnimation(holder);
ViewCompat.setAlpha(holder.itemView, 0);
ViewCompat.setTranslationX(holder.itemView, -holder.itemView.getWidth() / 2);
mPendingAdditions.add(holder);
return true;
}
private void animateAddImpl(final ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mAddAnimations.add(holder);
animation.alpha(1).translationX(0).setDuration(getAddDuration())
.setInterpolator(new DecelerateInterpolator()).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchAddStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationX(view, 0);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchAddFinished(holder);
mAddAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
int toX, int toY) {
final View view = holder.itemView;
fromX += ViewCompat.getTranslationX(holder.itemView);
fromY += ViewCompat.getTranslationY(holder.itemView);
int deltaX = toX - fromX;
int deltaY = toY - fromY;
if (deltaX == 0 && deltaY == 0) {
dispatchMoveFinished(holder);
return false;
}
resetAnimation(holder);
if (deltaX != 0) {
ViewCompat.setTranslationX(view, -deltaX);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, -deltaY);
}
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
return true;
}
private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
final View view = holder.itemView;
final int deltaX = toX - fromX;
final int deltaY = toY - fromY;
if (deltaX != 0) {
ViewCompat.animate(view).translationX(0);
}
if (deltaY != 0) {
ViewCompat.animate(view).translationY(0);
}
// TODO: make EndActions end listeners instead, since end actions aren't called when
// vpas are canceled (and can't end them. why?)
// need listener functionality in VPACompat for this. Ick.
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mMoveAnimations.add(holder);
animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchMoveStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
if (deltaX != 0) {
ViewCompat.setTranslationX(view, 0);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, 0);
}
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchMoveFinished(holder);
mMoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
// if (oldHolder != newHolder) {
// if (oldHolder != null) {
// dispatchChangeFinished(oldHolder, true);
// }
// if (newHolder != null) {
// dispatchChangeFinished(newHolder, false);
// }
// } else if (oldHolder != null) {
// dispatchChangeFinished(oldHolder, true);
// }
// return false;
if (oldHolder == newHolder) {
// Don't know how to run change animations when the same view holder is re-used.
// run a move animation to handle position changes.
if ((fromX - toX) == 0 && (fromY - toY) == 0) {
dispatchMoveFinished(oldHolder);
return false;
}
return animateMove(oldHolder, fromX, fromY, toX, toY);
}
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
resetAnimation(oldHolder);
int deltaX = (int) (toX - fromX - prevTranslationX);
int deltaY = (int) (toY - fromY - prevTranslationY);
// recover prev translation state after ending animation
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
if (newHolder != null) {
// carry over translation values
resetAnimation(newHolder);
ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
ViewCompat.setAlpha(newHolder.itemView, 0);
}
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
return true;
}
private void animateChangeImpl(final ChangeInfo changeInfo) {
final ViewHolder holder = changeInfo.oldHolder;
final View view = holder == null ? null : holder.itemView;
final ViewHolder newHolder = changeInfo.newHolder;
final View newView = newHolder != null ? newHolder.itemView : null;
if (view != null) {
final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
getChangeDuration());
mChangeAnimations.add(changeInfo.oldHolder);
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.oldHolder, true);
}
@Override
public void onAnimationEnd(View view) {
oldViewAnim.setListener(null);
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationX(view, 0);
ViewCompat.setTranslationY(view, 0);
dispatchChangeFinished(changeInfo.oldHolder, true);
mChangeAnimations.remove(changeInfo.oldHolder);
dispatchFinishedWhenDone();
}
}).start();
}
if (newView != null) {
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
mChangeAnimations.add(changeInfo.newHolder);
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
alpha(1).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.newHolder, false);
}
@Override
public void onAnimationEnd(View view) {
newViewAnimation.setListener(null);
ViewCompat.setAlpha(newView, 1);
ViewCompat.setTranslationX(newView, 0);
ViewCompat.setTranslationY(newView, 0);
dispatchChangeFinished(changeInfo.newHolder, false);
mChangeAnimations.remove(changeInfo.newHolder);
dispatchFinishedWhenDone();
}
}).start();
}
}
private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
for (int i = infoList.size() - 1; i >= 0; i--) {
ChangeInfo changeInfo = infoList.get(i);
if (endChangeAnimationIfNecessary(changeInfo, item)) {
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
infoList.remove(changeInfo);
}
}
}
}
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
if (changeInfo.oldHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
}
if (changeInfo.newHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
}
}
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
boolean oldItem = false;
if (changeInfo.newHolder == item) {
changeInfo.newHolder = null;
} else if (changeInfo.oldHolder == item) {
changeInfo.oldHolder = null;
oldItem = true;
} else {
return false;
}
ViewCompat.setAlpha(item.itemView, 1);
ViewCompat.setTranslationX(item.itemView, 0);
ViewCompat.setTranslationY(item.itemView, 0);
dispatchChangeFinished(item, oldItem);
return true;
}
@Override
public void endAnimation(ViewHolder item) {
final View view = item.itemView;
// this will trigger end callback which should set properties to their target values.
ViewCompat.animate(view).cancel();
// TODO if some other animations are chained to end, how do we cancel them as well?
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
MoveInfo moveInfo = mPendingMoves.get(i);
if (moveInfo.holder == item) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item);
mPendingMoves.remove(i);
}
}
endChangeAnimation(mPendingChanges, item);
if (mPendingRemovals.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchRemoveFinished(item);
}
if (mPendingAdditions.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
}
for (int i = mChangesList.size() - 1; i >= 0; i--) {
ArrayList<ChangeInfo> changes = mChangesList.get(i);
endChangeAnimation(changes, item);
if (changes.isEmpty()) {
mChangesList.remove(i);
}
}
for (int i = mMovesList.size() - 1; i >= 0; i--) {
ArrayList<MoveInfo> moves = mMovesList.get(i);
for (int j = moves.size() - 1; j >= 0; j--) {
MoveInfo moveInfo = moves.get(j);
if (moveInfo.holder == item) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item);
moves.remove(j);
if (moves.isEmpty()) {
mMovesList.remove(i);
}
break;
}
}
}
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
if (additions.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
if (additions.isEmpty()) {
mAdditionsList.remove(i);
}
}
}
// animations should be ended by the cancel above.
//noinspection PointlessBooleanExpression,ConstantConditions
if (mRemoveAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mRemoveAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mAddAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mAddAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mChangeAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mChangeAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mMoveAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mMoveAnimations list");
}
dispatchFinishedWhenDone();
}
private void resetAnimation(ViewHolder holder) {
AnimatorCompatHelper.clearInterpolator(holder.itemView);
endAnimation(holder);
}
@Override
public boolean isRunning() {
return (!mPendingAdditions.isEmpty() ||
!mPendingChanges.isEmpty() ||
!mPendingMoves.isEmpty() ||
!mPendingRemovals.isEmpty() ||
!mMoveAnimations.isEmpty() ||
!mRemoveAnimations.isEmpty() ||
!mAddAnimations.isEmpty() ||
!mChangeAnimations.isEmpty() ||
!mMovesList.isEmpty() ||
!mAdditionsList.isEmpty() ||
!mChangesList.isEmpty());
}
/**
* Check the state of currently pending and running animations. If there are none
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
* listeners.
*/
private void dispatchFinishedWhenDone() {
if (!isRunning()) {
dispatchAnimationsFinished();
}
}
@Override
public void endAnimations() {
int count = mPendingMoves.size();
for (int i = count - 1; i >= 0; i--) {
MoveInfo item = mPendingMoves.get(i);
View view = item.holder.itemView;
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item.holder);
mPendingMoves.remove(i);
}
count = mPendingRemovals.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mPendingRemovals.get(i);
dispatchRemoveFinished(item);
mPendingRemovals.remove(i);
}
count = mPendingAdditions.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mPendingAdditions.get(i);
View view = item.itemView;
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
mPendingAdditions.remove(i);
}
count = mPendingChanges.size();
for (int i = count - 1; i >= 0; i--) {
endChangeAnimationIfNecessary(mPendingChanges.get(i));
}
mPendingChanges.clear();
if (!isRunning()) {
return;
}
int listCount = mMovesList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<MoveInfo> moves = mMovesList.get(i);
count = moves.size();
for (int j = count - 1; j >= 0; j--) {
MoveInfo moveInfo = moves.get(j);
ViewHolder item = moveInfo.holder;
View view = item.itemView;
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(moveInfo.holder);
moves.remove(j);
if (moves.isEmpty()) {
mMovesList.remove(moves);
}
}
}
listCount = mAdditionsList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
count = additions.size();
for (int j = count - 1; j >= 0; j--) {
ViewHolder item = additions.get(j);
View view = item.itemView;
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
additions.remove(j);
if (additions.isEmpty()) {
mAdditionsList.remove(additions);
}
}
}
listCount = mChangesList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<ChangeInfo> changes = mChangesList.get(i);
count = changes.size();
for (int j = count - 1; j >= 0; j--) {
endChangeAnimationIfNecessary(changes.get(j));
if (changes.isEmpty()) {
mChangesList.remove(changes);
}
}
}
cancelAll(mRemoveAnimations);
cancelAll(mMoveAnimations);
cancelAll(mAddAnimations);
cancelAll(mChangeAnimations);
dispatchAnimationsFinished();
}
static void cancelAll(List<ViewHolder> viewHolders) {
for (int i = viewHolders.size() - 1; i >= 0; i--) {
ViewCompat.animate(viewHolders.get(i).itemView).cancel();
}
}
private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
@Override
public void onAnimationStart(View view) {}
@Override
public void onAnimationEnd(View view) {}
@Override
public void onAnimationCancel(View view) {}
}
}
@@ -1,30 +0,0 @@
/*
* Copyright 2014 A.C.R. Development
*/
package acr.browser.lightning.object;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import acr.browser.lightning.controller.BrowserController;
public class ClickHandler extends Handler {
private BrowserController mBrowserController;
public ClickHandler(Context context) {
try {
mBrowserController = (BrowserController) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement BrowserController");
}
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
String url = msg.getData().getString("url");
mBrowserController.longClickPage(url);
}
}
@@ -1,11 +1,17 @@
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.app.BrowserApp;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.download.DownloadHandler;
@Singleton
public class PreferenceManager {
private static class Name {
@@ -29,7 +35,6 @@ public class PreferenceManager {
public static final String SEARCH_URL = "searchurl";
public static final String TEXT_REFLOW = "textreflow";
public static final String TEXT_SIZE = "textsize";
public static final String URL_MEMORY = "memory";
public static final String USE_WIDE_VIEWPORT = "wideviewport";
public static final String USER_AGENT = "agentchoose";
public static final String USER_AGENT_STRING = "userAgentString";
@@ -47,6 +52,8 @@ public class PreferenceManager {
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";
@@ -54,22 +61,17 @@ public class PreferenceManager {
public static final String USE_PROXY_PORT = "useProxyPort";
public static final String INITIAL_CHECK_FOR_TOR = "checkForTor";
public static final String INITIAL_CHECK_FOR_I2P = "checkForI2P";
public static final String LEAK_CANARY = "leakCanary";
}
private static PreferenceManager mInstance;
private final SharedPreferences mPrefs;
@NonNull private final SharedPreferences mPrefs;
private static final String PREFERENCES = "settings";
public static PreferenceManager getInstance() {
if (mInstance == null) {
mInstance = new PreferenceManager();
}
return mInstance;
}
private PreferenceManager() {
mPrefs = BrowserApp.getAppContext().getSharedPreferences(PREFERENCES, 0);
@Inject
PreferenceManager(@NonNull final Context context) {
mPrefs = context.getSharedPreferences(PREFERENCES, 0);
}
public boolean getAdBlockEnabled() {
@@ -109,13 +111,14 @@ public class PreferenceManager {
}
public boolean getColorModeEnabled() {
return mPrefs.getBoolean(Name.ENABLE_COLOR_MODE, false);
return mPrefs.getBoolean(Name.ENABLE_COLOR_MODE, true);
}
public boolean getCookiesEnabled() {
return mPrefs.getBoolean(Name.COOKIES, true);
}
@NonNull
public String getDownloadDirectory() {
return mPrefs.getString(Name.DOWNLOAD_DIRECTORY, DownloadHandler.DEFAULT_DOWNLOAD_PATH);
}
@@ -136,6 +139,7 @@ public class PreferenceManager {
return mPrefs.getBoolean(Name.HIDE_STATUS_BAR, false);
}
@NonNull
public String getHomepage() {
return mPrefs.getString(Name.HOMEPAGE, Constants.HOMEPAGE);
}
@@ -156,10 +160,6 @@ public class PreferenceManager {
return mPrefs.getBoolean(Name.LOCATION, false);
}
public String getMemoryUrl() {
return mPrefs.getString(Name.URL_MEMORY, "");
}
public boolean getOverviewModeEnabled() {
return mPrefs.getBoolean(Name.OVERVIEW_MODE, true);
}
@@ -168,6 +168,7 @@ public class PreferenceManager {
return mPrefs.getBoolean(Name.POPUPS, true);
}
@NonNull
public String getProxyHost() {
return mPrefs.getString(Name.USE_PROXY_HOST, "localhost");
}
@@ -188,6 +189,7 @@ public class PreferenceManager {
return mPrefs.getBoolean(Name.RESTORE_LOST_TABS, true);
}
@Nullable
public String getSavedUrl() {
return mPrefs.getString(Name.SAVE_URL, null);
}
@@ -200,6 +202,7 @@ public class PreferenceManager {
return mPrefs.getInt(Name.SEARCH, 1);
}
@NonNull
public String getSearchUrl() {
return mPrefs.getString(Name.SEARCH_URL, Constants.GOOGLE_SEARCH);
}
@@ -232,7 +235,8 @@ public class PreferenceManager {
return mPrefs.getInt(Name.USER_AGENT, 1);
}
public String getUserAgentString(String def) {
@Nullable
public String getUserAgentString(@Nullable String def) {
return mPrefs.getString(Name.USER_AGENT_STRING, def);
}
@@ -240,6 +244,7 @@ public class PreferenceManager {
return mPrefs.getBoolean(Name.USE_WIDE_VIEWPORT, true);
}
@NonNull
public String getTextEncoding() {
return mPrefs.getString(Name.TEXT_ENCODING, Constants.DEFAULT_ENCODING);
}
@@ -248,23 +253,39 @@ public class PreferenceManager {
return mPrefs.getBoolean(Name.SHOW_TABS_IN_DRAWER, defaultValue);
}
private void putBoolean(String name, boolean value) {
public boolean getDoNotTrackEnabled() {
return mPrefs.getBoolean(Name.DO_NOT_TRACK, false);
}
public boolean getRemoveIdentifyingHeadersEnabled() {
return mPrefs.getBoolean(Name.IDENTIFYING_HEADERS, false);
}
private void putBoolean(@NonNull String name, boolean value) {
mPrefs.edit().putBoolean(name, value).apply();
}
private void putInt(String name, int value) {
private void putInt(@NonNull String name, int value) {
mPrefs.edit().putInt(name, value).apply();
}
private void putString(String name, String value) {
private void putString(@NonNull String name, @Nullable String value) {
mPrefs.edit().putString(name, value).apply();
}
public void setRemoveIdentifyingHeadersEnabled(boolean enabled) {
putBoolean(Name.IDENTIFYING_HEADERS, enabled);
}
public void setDoNotTrackEnabled(boolean doNotTrack) {
putBoolean(Name.DO_NOT_TRACK, doNotTrack);
}
public void setShowTabsInDrawer(boolean show) {
putBoolean(Name.SHOW_TABS_IN_DRAWER, show);
}
public void setTextEncoding(String encoding) {
public void setTextEncoding(@NonNull String encoding) {
putString(Name.TEXT_ENCODING, encoding);
}
@@ -312,7 +333,7 @@ public class PreferenceManager {
putBoolean(Name.COOKIES, enable);
}
public void setDownloadDirectory(String directory) {
public void setDownloadDirectory(@NonNull String directory) {
putString(Name.DOWNLOAD_DIRECTORY, directory);
}
@@ -332,7 +353,7 @@ public class PreferenceManager {
putBoolean(Name.HIDE_STATUS_BAR, enable);
}
public void setHomepage(String homepage) {
public void setHomepage(@NonNull String homepage) {
putString(Name.HOMEPAGE, homepage);
}
@@ -352,10 +373,6 @@ public class PreferenceManager {
putBoolean(Name.LOCATION, enable);
}
public void setMemoryUrl(String url) {
putString(Name.URL_MEMORY, url);
}
public void setOverviewModeEnabled(boolean enable) {
putBoolean(Name.OVERVIEW_MODE, enable);
}
@@ -376,7 +393,7 @@ public class PreferenceManager {
putBoolean(Name.RESTORE_LOST_TABS, enable);
}
public void setSavedUrl(String url) {
public void setSavedUrl(@Nullable String url) {
putString(Name.SAVE_URL, url);
}
@@ -388,7 +405,7 @@ public class PreferenceManager {
putInt(Name.SEARCH, choice);
}
public void setSearchUrl(String url) {
public void setSearchUrl(@NonNull String url) {
putString(Name.SEARCH_URL, url);
}
@@ -408,6 +425,14 @@ public class PreferenceManager {
putInt(Name.THEME, theme);
}
public void setUseLeakCanary(boolean useLeakCanary) {
putBoolean(Name.LEAK_CANARY, useLeakCanary);
}
public boolean getUseLeakCanary() {
return mPrefs.getBoolean(Name.LEAK_CANARY, false);
}
/**
* Valid choices:
* <ul>
@@ -423,7 +448,7 @@ public class PreferenceManager {
putInt(Name.PROXY_CHOICE, choice);
}
public void setProxyHost(String proxyHost) {
public void setProxyHost(@NonNull String proxyHost) {
putString(Name.USE_PROXY_HOST, proxyHost);
}
@@ -435,7 +460,7 @@ public class PreferenceManager {
putInt(Name.USER_AGENT, choice);
}
public void setUserAgentString(String agent) {
public void setUserAgentString(@Nullable String agent) {
putString(Name.USER_AGENT_STRING, agent);
}
@@ -0,0 +1,16 @@
package acr.browser.lightning.react;
import android.support.annotation.NonNull;
public interface Action<T> {
/**
* Should be overridden to send the subscriber
* events such as {@link Subscriber#onNext(Object)}
* or {@link Subscriber#onComplete()}.
*
* @param subscriber the subscriber that is sent in
* when the user of the Observable
* subscribes.
*/
void onSubscribe(@NonNull Subscriber<T> subscriber);
}
@@ -0,0 +1,254 @@
package acr.browser.lightning.react;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.util.concurrent.Executor;
import acr.browser.lightning.utils.Preconditions;
/**
* An RxJava implementation. This class allows work
* to be done on a certain thread and then allows
* items to be emitted on a different thread. It is
* a replacement for {@link android.os.AsyncTask}.
*
* @param <T> the type that the Observable will emit.
*/
public class Observable<T> {
private static final String TAG = Observable.class.getSimpleName();
@NonNull private final Action<T> mAction;
@Nullable private Executor mSubscriberThread;
@Nullable private Executor mObserverThread;
@NonNull private final Executor mDefault;
private Observable(@NonNull Action<T> action) {
mAction = action;
Looper looper = Looper.myLooper();
Preconditions.checkNonNull(looper);
mDefault = new ThreadExecutor(looper);
}
/**
* Static creator method that creates an Observable from the
* {@link Action} that is passed in as the parameter. Action
* must not be null.
*
* @param action the Action to perform
* @param <T> the type that will be emitted to the onSubscribe
* @return a valid non-null Observable.
*/
@NonNull
public static <T> Observable<T> create(@NonNull Action<T> action) {
Preconditions.checkNonNull(action);
return new Observable<>(action);
}
/**
* Tells the Observable what Executor that the onSubscribe
* work should run on.
*
* @param subscribeExecutor the Executor to run the work on.
* @return returns this so that calls can be conveniently chained.
*/
public Observable<T> subscribeOn(@NonNull Executor subscribeExecutor) {
mSubscriberThread = subscribeExecutor;
return this;
}
/**
* Tells the Observable what Executor the onSubscribe should observe
* the work on.
*
* @param observerExecutor the Executor to run to callback on.
* @return returns this so that calls can be conveniently chained.
*/
public Observable<T> observeOn(@NonNull Executor observerExecutor) {
mObserverThread = observerExecutor;
return this;
}
/**
* Subscribes immediately to the Observable and ignores
* all onComplete and onNext calls.
*/
public void subscribe() {
executeOnSubscriberThread(new Runnable() {
@Override
public void run() {
mAction.onSubscribe(new Subscriber<T>() {
@Override
public void unsubscribe() {}
@Override
public void onComplete() {}
@Override
public void onStart() {}
@Override
public void onError(@NonNull Throwable throwable) {}
@Override
public void onNext(T item) {}
});
}
});
}
/**
* Immediately subscribes to the Observable and starts
* sending events from the Observable to the {@link OnSubscribe}.
*
* @param onSubscribe the class that wishes to receive onNext and
* onComplete callbacks from the Observable.
*/
public Subscription subscribe(@NonNull OnSubscribe<T> onSubscribe) {
Preconditions.checkNonNull(onSubscribe);
final Subscriber<T> subscriber = new SubscriberImpl<>(onSubscribe, this);
subscriber.onStart();
executeOnSubscriberThread(new Runnable() {
@Override
public void run() {
mAction.onSubscribe(subscriber);
}
});
return subscriber;
}
private void executeOnObserverThread(@NonNull Runnable runnable) {
if (mObserverThread != null) {
mObserverThread.execute(runnable);
} else {
mDefault.execute(runnable);
}
}
private void executeOnSubscriberThread(@NonNull Runnable runnable) {
if (mSubscriberThread != null) {
mSubscriberThread.execute(runnable);
} else {
mDefault.execute(runnable);
}
}
private static class SubscriberImpl<T> implements Subscriber<T> {
@Nullable private volatile OnSubscribe<T> mOnSubscribe;
@NonNull private final Observable<T> mObservable;
private boolean mOnCompleteExecuted = false;
private boolean mOnError = false;
public SubscriberImpl(@NonNull OnSubscribe<T> onSubscribe, @NonNull Observable<T> observable) {
mOnSubscribe = onSubscribe;
mObservable = observable;
}
@Override
public void unsubscribe() {
mOnSubscribe = null;
}
@Override
public void onComplete() {
OnSubscribe<T> onSubscribe = mOnSubscribe;
if (!mOnCompleteExecuted && onSubscribe != null && !mOnError) {
mOnCompleteExecuted = true;
mObservable.executeOnObserverThread(new OnCompleteRunnable<>(onSubscribe));
} else if (!mOnError) {
Log.e(TAG, "onComplete called more than once");
throw new RuntimeException("onComplete called more than once");
}
}
@Override
public void onStart() {
OnSubscribe<T> onSubscribe = mOnSubscribe;
if (onSubscribe != null) {
mObservable.executeOnObserverThread(new OnStartRunnable<>(onSubscribe));
}
}
@Override
public void onError(@NonNull final Throwable throwable) {
OnSubscribe<T> onSubscribe = mOnSubscribe;
if (onSubscribe != null) {
mOnError = true;
mObservable.executeOnObserverThread(new OnErrorRunnable<>(onSubscribe, throwable));
}
}
@Override
public void onNext(final T item) {
OnSubscribe<T> onSubscribe = mOnSubscribe;
if (!mOnCompleteExecuted && onSubscribe != null) {
mObservable.executeOnObserverThread(new OnNextRunnable<>(onSubscribe, item));
} else {
Log.e(TAG, "onComplete has been already called, onNext should not be called");
throw new RuntimeException("onNext should not be called after onComplete has been called");
}
}
}
private static class OnCompleteRunnable<T> implements Runnable {
private final OnSubscribe<T> onSubscribe;
public OnCompleteRunnable(@NonNull OnSubscribe<T> onSubscribe) {this.onSubscribe = onSubscribe;}
@Override
public void run() {
onSubscribe.onComplete();
}
}
private static class OnNextRunnable<T> implements Runnable {
private final OnSubscribe<T> onSubscribe;
private final T item;
public OnNextRunnable(@NonNull OnSubscribe<T> onSubscribe, T item) {
this.onSubscribe = onSubscribe;
this.item = item;
}
@Override
public void run() {
onSubscribe.onNext(item);
}
}
private static class OnErrorRunnable<T> implements Runnable {
private final OnSubscribe<T> onSubscribe;
private final Throwable throwable;
public OnErrorRunnable(@NonNull OnSubscribe<T> onSubscribe, @NonNull Throwable throwable) {
this.onSubscribe = onSubscribe;
this.throwable = throwable;
}
@Override
public void run() {
onSubscribe.onError(throwable);
}
}
private static class OnStartRunnable<T> implements Runnable {
private final OnSubscribe<T> onSubscribe;
public OnStartRunnable(@NonNull OnSubscribe<T> onSubscribe) {this.onSubscribe = onSubscribe;}
@Override
public void run() {
onSubscribe.onStart();
}
}
}
@@ -0,0 +1,47 @@
package acr.browser.lightning.react;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public abstract class OnSubscribe<T> {
/**
* Called when the observable
* runs into an error that will
* cause it to abort and not finish.
* Receiving this callback means that
* the observable is dead and no
* {@link #onComplete()} or {@link #onNext(Object)}
* callbacks will be called.
*
* @param throwable an optional throwable that could
* be sent.
*/
public void onError(@NonNull Throwable throwable) {}
/**
* Called before the observer begins
* to process and emit items or complete.
*/
public void onStart() {}
/**
* Called when the Observer emits an
* item. It can be called multiple times.
* It cannot be called after onComplete
* has been called.
*
* @param item the item that has been emitted,
* can be null.
*/
public void onNext(@Nullable T item) {}
/**
* This method is called when the observer is
* finished sending the subscriber events. It
* is guaranteed that no other methods will be
* called on the OnSubscribe after this method
* has been called.
*/
public void onComplete() {}
}
@@ -0,0 +1,46 @@
package acr.browser.lightning.react;
import android.os.Looper;
import android.support.annotation.NonNull;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class Schedulers {
private static final Executor sWorker = Executors.newFixedThreadPool(4);
private static final Executor sIOWorker = Executors.newSingleThreadExecutor();
private static final Executor sMain = new ThreadExecutor(Looper.getMainLooper());
/**
* The worker thread executor, will
* execute work on any one of multiple
* threads.
*
* @return a non-null executor.
*/
@NonNull
public static Executor worker() {
return sWorker;
}
/**
* The main thread.
*
* @return a non-null executor that does work on the main thread.
*/
@NonNull
public static Executor main() {
return sMain;
}
/**
* The io thread.
*
* @return a non-null executor that does
* work on a single thread off the main thread.
*/
@NonNull
public static Executor io() {
return sIOWorker;
}
}
@@ -0,0 +1,51 @@
package acr.browser.lightning.react;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
public interface Subscriber<T> extends Subscription {
/**
* Called immediately upon subscribing
* and before the Observable begins
* emitting items. This should not be
* called by the creator of the Observable
* and is rather called internally by the
* Observable class itself.
*/
void onStart();
/**
* Called when the observable
* runs into an error that will
* cause it to abort and not finish.
* Receiving this callback means that
* the observable is dead and no
* {@link #onComplete()} or {@link #onNext(Object)}
* callbacks will be called.
*
* @param throwable an optional throwable that could
* be sent.
*/
void onError(@NonNull Throwable throwable);
/**
* Called when the Observer emits an
* item. It can be called multiple times.
* It cannot be called after onComplete
* has been called.
*
* @param item the item that has been emitted,
* can be null.
*/
void onNext(@Nullable T item);
/**
* This method is called when the observer is
* finished sending the subscriber events. It
* is guaranteed that no other methods will be
* called on the OnSubscribe after this method
* has been called.
*/
void onComplete();
}
@@ -0,0 +1,7 @@
package acr.browser.lightning.react;
public interface Subscription {
void unsubscribe();
}
@@ -0,0 +1,21 @@
package acr.browser.lightning.react;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import java.util.concurrent.Executor;
class ThreadExecutor implements Executor {
private final Handler mHandler;
public ThreadExecutor(@NonNull Looper looper) {
mHandler = new Handler(looper);
}
@Override
public void execute(@NonNull Runnable command) {
mHandler.post(command);
}
}
@@ -491,8 +491,7 @@ public class ArticleTextExtractor {
Element el = elems.get(0);
if (el.hasAttr("content")) {
dateStr = el.attr("content");
Date parsedDate = parseDate(dateStr);
return parsedDate;
return parseDate(dateStr);
}
}
@@ -33,6 +33,8 @@ 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.
*
@@ -49,28 +51,36 @@ public class HtmlFetcher {
}
public static void main(String[] args) throws Exception {
BufferedReader reader = new BufferedReader(new FileReader("urls.txt"));
String line;
Set<String> existing = new LinkedHashSet<>();
while ((line = reader.readLine()) != null) {
int index1 = line.indexOf('\"');
int index2 = line.indexOf('\"', index1 + 1);
String url = line.substring(index1 + 1, index2);
String domainStr = SHelper.extractDomain(url, true);
String counterStr = "";
// TODO more similarities
if (existing.contains(domainStr))
counterStr = "2";
else
existing.add(domainStr);
BufferedReader reader = null;
BufferedWriter writer = null;
try {
String html = new HtmlFetcher().fetchAsString(url, 2000);
String outFile = domainStr + counterStr + ".html";
BufferedWriter writer = new BufferedWriter(new FileWriter(outFile));
writer.write(html);
writer.close();
//noinspection IOResourceOpenedButNotSafelyClosed
reader = new BufferedReader(new FileReader("urls.txt"));
String line;
Set<String> existing = new LinkedHashSet<>();
while ((line = reader.readLine()) != null) {
int index1 = line.indexOf('\"');
int index2 = line.indexOf('\"', index1 + 1);
String url = line.substring(index1 + 1, index2);
String domainStr = SHelper.extractDomain(url, true);
String counterStr = "";
// TODO more similarities
if (existing.contains(domainStr))
counterStr = "2";
else
existing.add(domainStr);
String html = new HtmlFetcher().fetchAsString(url, 2000);
String outFile = domainStr + counterStr + ".html";
//noinspection IOResourceOpenedButNotSafelyClosed
writer = new BufferedWriter(new FileWriter(outFile));
writer.write(html);
}
} finally {
Utils.close(reader);
Utils.close(writer);
}
reader.close();
}
private String referrer = "http://jetsli.de/crawler";
@@ -386,8 +396,8 @@ public class HtmlFetcher {
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.startsWith("http://bit.ly")
|| urlAsString.startsWith("http://is.gd"))
if (urlAsString.contains("://bit.ly")
|| urlAsString.contains("://is.gd"))
newUrl = encodeUriFromHeader(newUrl);
// AP: This code is not longer need, instead we always follow
@@ -1,12 +1,12 @@
/**
* Copyright (C) 2010 Peter Karich <>
*
* <p/>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
*
* <p/>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -20,7 +20,7 @@ import java.util.Map;
/**
* Simple impl of Map.Entry. So that we can have ordered maps.
*
*
* @author Peter Karich, peat_hal at users dot sourceforge dot
* net
*/
@@ -60,13 +60,12 @@ public class MapEntry<K, V> implements Map.Entry<K, V>, Serializable {
public boolean equals(Object obj) {
if (obj == null)
return false;
if (getClass() != obj.getClass())
if (!(obj instanceof Map<?, ?>))
return false;
final MapEntry<K, V> other = (MapEntry<K, V>) obj;
if (this.key != other.key && (this.key == null || !this.key.equals(other.key)))
return false;
return !(this.value != other.value && (this.value == null || !this.value
.equals(other.value)));
final MapEntry<?, ?> other = (MapEntry<?, ?>) obj;
return !(this.key != other.key && (this.key == null || !this.key.equals(other.key))) &&
!(this.value != other.value && (this.value == null || !this.value.equals(other.value)));
}
@Override
@@ -23,7 +23,7 @@ public class OutputFormatter {
private static final int MIN_FIRST_PARAGRAPH_TEXT = 50; // Min size of first paragraph
private static final int MIN_PARAGRAPH_TEXT = 30; // Min size of any other paragraphs
private static final List<String> NODES_TO_REPLACE = Arrays.asList("strong", "b", "i");
private Pattern unlikelyPattern = Pattern.compile("display\\:none|visibility\\:hidden");
private Pattern unlikelyPattern = Pattern.compile("display:none|visibility:hidden");
private final int minFirstParagraphText;
private final int minParagraphText;
private final List<String> nodesToReplace;
@@ -251,9 +251,9 @@ class SHelper {
}
public static String getUrlFromUglyGoogleRedirect(String url) {
if (url.startsWith("http://www.google.com/url?")) {
url = url.substring("http://www.google.com/url?".length());
String arr[] = urlDecode(url).split("\\&");
if (url.startsWith("https://www.google.com/url?")) {
url = url.substring("https://www.google.com/url?".length());
String arr[] = urlDecode(url).split("&");
for (String str : arr) {
if (str.startsWith("q="))
return str.substring("q=".length());
@@ -264,8 +264,8 @@ class SHelper {
}
public static String getUrlFromUglyFacebookRedirect(String url) {
if (url.startsWith("http://www.facebook.com/l.php?u=")) {
url = url.substring("http://www.facebook.com/l.php?u=".length());
if (url.startsWith("https://www.facebook.com/l.php?u=")) {
url = url.substring("https://www.facebook.com/l.php?u=".length());
return urlDecode(url);
}
@@ -5,14 +5,14 @@ import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.support.annotation.NonNull;
public class NetworkReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
}
public void onReceive(Context context, Intent intent) {}
public static boolean isConnected(Context context) {
public static boolean isConnected(@NonNull Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null)
return false;
@@ -0,0 +1,201 @@
package acr.browser.lightning.search;
import android.app.Application;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import acr.browser.lightning.R;
import acr.browser.lightning.database.HistoryItem;
import acr.browser.lightning.utils.Utils;
class RetrieveSuggestionsTask extends AsyncTask<Void, Void, List<HistoryItem>> {
private static final String TAG = RetrieveSuggestionsTask.class.getSimpleName();
private static final Pattern SPACE_PATTERN = Pattern.compile(" ", Pattern.LITERAL);
private static final String CACHE_FILE_TYPE = ".sgg";
private static final String ENCODING = "ISO-8859-1";
private static final long INTERVAL_DAY = 86400000;
private static final String DEFAULT_LANGUAGE = "en";
@Nullable private static XmlPullParser sXpp;
@Nullable private static String sLanguage;
@NonNull private final WeakReference<SuggestionsResult> mResultCallback;
@NonNull private final Application mApplication;
@NonNull private final String mSearchSubtitle;
@NonNull private String mQuery;
public RetrieveSuggestionsTask(@NonNull String query,
@NonNull SuggestionsResult callback,
@NonNull Application application) {
mQuery = query;
mResultCallback = new WeakReference<>(callback);
mApplication = application;
mSearchSubtitle = mApplication.getString(R.string.suggestion);
}
@NonNull
private static synchronized String getLanguage() {
if (sLanguage == null) {
sLanguage = Locale.getDefault().getLanguage();
}
if (TextUtils.isEmpty(sLanguage)) {
sLanguage = DEFAULT_LANGUAGE;
}
return sLanguage;
}
@NonNull
private static synchronized XmlPullParser getParser() throws XmlPullParserException {
if (sXpp == null) {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
sXpp = factory.newPullParser();
}
return sXpp;
}
@NonNull
@Override
protected List<HistoryItem> doInBackground(Void... voids) {
List<HistoryItem> filter = new ArrayList<>(5);
try {
mQuery = SPACE_PATTERN.matcher(mQuery).replaceAll("+");
URLEncoder.encode(mQuery, ENCODING);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
File cache = downloadSuggestionsForQuery(mQuery, getLanguage(), mApplication);
if (!cache.exists()) {
return filter;
}
InputStream fileInput = null;
try {
fileInput = new BufferedInputStream(new FileInputStream(cache));
XmlPullParser parser = getParser();
parser.setInput(fileInput, ENCODING);
int eventType = parser.getEventType();
int counter = 0;
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG && "suggestion".equals(parser.getName())) {
String suggestion = parser.getAttributeValue(null, "data");
filter.add(new HistoryItem(mSearchSubtitle + " \"" + suggestion + '"',
suggestion, R.drawable.ic_search));
counter++;
if (counter >= 5) {
break;
}
}
eventType = parser.next();
}
} catch (Exception e) {
return filter;
} finally {
Utils.close(fileInput);
}
return filter;
}
@Override
protected void onPostExecute(@NonNull List<HistoryItem> result) {
SuggestionsResult callback = mResultCallback.get();
if (callback != null) {
callback.resultReceived(result);
}
}
/**
* This method downloads the search suggestions for the specific query.
* NOTE: This is a blocking operation, do not run on the UI thread.
*
* @param query the query to get suggestions for
* @return the cache file containing the suggestions
*/
@NonNull
private static File downloadSuggestionsForQuery(@NonNull String query, String language, @NonNull Application app) {
File cacheFile = new File(app.getCacheDir(), query.hashCode() + CACHE_FILE_TYPE);
if (System.currentTimeMillis() - INTERVAL_DAY < cacheFile.lastModified()) {
return cacheFile;
}
if (!isNetworkConnected(app)) {
return cacheFile;
}
InputStream in = null;
FileOutputStream fos = null;
try {
// Old API that doesn't support HTTPS
// http://google.com/complete/search?q= + query + &output=toolbar&hl= + language
URL url = new URL("https://suggestqueries.google.com/complete/search?output=toolbar&hl="
+ language + "&q=" + query);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.connect();
if (connection.getResponseCode() >= HttpURLConnection.HTTP_MULT_CHOICE ||
connection.getResponseCode() < HttpURLConnection.HTTP_OK) {
Log.e(TAG, "Search API Responded with code: " + connection.getResponseCode());
connection.disconnect();
return cacheFile;
}
in = connection.getInputStream();
if (in != null) {
//noinspection IOResourceOpenedButNotSafelyClosed
fos = new FileOutputStream(cacheFile);
int buffer;
while ((buffer = in.read()) != -1) {
fos.write(buffer);
}
fos.flush();
}
connection.disconnect();
cacheFile.setLastModified(System.currentTimeMillis());
} catch (Exception e) {
Log.w(TAG, "Problem getting search suggestions", e);
} finally {
Utils.close(in);
Utils.close(fos);
}
return cacheFile;
}
private static boolean isNetworkConnected(@NonNull Context context) {
NetworkInfo networkInfo = getActiveNetworkInfo(context);
return networkInfo != null && networkInfo.isConnected();
}
@Nullable
private static NetworkInfo getActiveNetworkInfo(@NonNull Context context) {
ConnectivityManager connectivity = (ConnectivityManager) context
.getApplicationContext()
.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivity == null) {
return null;
}
return connectivity.getActiveNetworkInfo();
}
}
@@ -1,11 +1,12 @@
package acr.browser.lightning.object;
package acr.browser.lightning.search;
import android.app.Application;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -15,26 +16,14 @@ import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.TextView;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import javax.inject.Inject;
@@ -45,86 +34,60 @@ import acr.browser.lightning.database.HistoryDatabase;
import acr.browser.lightning.database.HistoryItem;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.ThemeUtils;
import acr.browser.lightning.utils.Utils;
public class SearchAdapter extends BaseAdapter implements Filterable {
public class SuggestionsAdapter extends BaseAdapter implements Filterable, SuggestionsResult {
private static final String TAG = SuggestionsAdapter.class.getSimpleName();
private static final Pattern SPACE_PATTERN = Pattern.compile(" ", Pattern.LITERAL);
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 final Object mLock = new Object();
private HistoryDatabase mDatabaseHandler;
private final Context mContext;
private boolean mUseGoogle = true;
private boolean mIsExecuting = false;
private final boolean mDarkTheme;
private final boolean mIncognito;
@Inject
BookmarkManager mBookmarkManager;
private static final String CACHE_FILE_TYPE = ".sgg";
private static final String ENCODING = "ISO-8859-1";
private static final long INTERVAL_DAY = 86400000;
private static final int MAX_SUGGESTIONS = 5;
private static final SuggestionsComparator mComparator = new SuggestionsComparator();
private final String mSearchSubtitle;
private SearchFilter mFilter;
private final Drawable mSearchDrawable;
private final Drawable mHistoryDrawable;
private final Drawable mBookmarkDrawable;
private static final SuggestionsComparator sComparator = new SuggestionsComparator();
public SearchAdapter(Context context, boolean dark, boolean incognito) {
@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);
mDatabaseHandler = HistoryDatabase.getInstance();
mAllBookmarks.addAll(mBookmarkManager.getAllBookmarks(true));
mUseGoogle = PreferenceManager.getInstance().getGoogleSearchSuggestionsEnabled();
mUseGoogle = mPreferenceManager.getGoogleSearchSuggestionsEnabled();
mContext = context;
mSearchSubtitle = mContext.getString(R.string.suggestion);
mDarkTheme = dark || incognito;
mIncognito = incognito;
Thread delete = new Thread(new ClearCacheRunnable());
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);
delete.setPriority(Thread.MIN_PRIORITY);
delete.start();
}
private static void deleteOldCacheFiles() {
File dir = new File(BrowserApp.getAppContext().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, String filename) {
return filename.endsWith(CACHE_FILE_TYPE);
}
}
public void refreshPreferences() {
mUseGoogle = PreferenceManager.getInstance().getGoogleSearchSuggestionsEnabled();
mUseGoogle = mPreferenceManager.getGoogleSearchSuggestionsEnabled();
if (!mUseGoogle) {
synchronized (mSuggestions) {
mSuggestions.clear();
}
}
mDatabaseHandler = HistoryDatabase.getInstance();
}
public void refreshBookmarks() {
synchronized (mLock) {
synchronized (SuggestionsAdapter.this) {
mAllBookmarks.clear();
mAllBookmarks.addAll(mBookmarkManager.getAllBookmarks(true));
}
@@ -145,8 +108,15 @@ public class SearchAdapter extends BaseAdapter implements Filterable {
return 0;
}
private static class SuggestionHolder {
ImageView mImage;
TextView mTitle;
TextView mUrl;
}
@Nullable
@Override
public View getView(int position, View convertView, ViewGroup parent) {
public View getView(int position, @Nullable View convertView, ViewGroup parent) {
SuggestionHolder holder;
if (convertView == null) {
@@ -208,30 +178,56 @@ public class SearchAdapter extends BaseAdapter implements Filterable {
private static class ClearCacheRunnable implements Runnable {
@NonNull
private final Application app;
public ClearCacheRunnable(@NonNull Application app) {
this.app = app;
}
@Override
public void run() {
deleteOldCacheFiles();
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(CharSequence constraint) {
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) {
new RetrieveSearchSuggestions().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, query);
mIsExecuting = true;
new RetrieveSuggestionsTask(query, SuggestionsAdapter.this, BrowserApp.get(mContext)).executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
}
int counter = 0;
synchronized (mBookmarks) {
mBookmarks.clear();
synchronized (mLock) {
synchronized (SuggestionsAdapter.this) {
for (int n = 0; n < mAllBookmarks.size(); n++) {
if (counter >= 5) {
break;
@@ -244,13 +240,10 @@ public class SearchAdapter extends BaseAdapter implements Filterable {
mBookmarks.add(mAllBookmarks.get(n));
counter++;
}
}
}
}
if (mDatabaseHandler == null || mDatabaseHandler.isClosed()) {
mDatabaseHandler = HistoryDatabase.getInstance();
}
List<HistoryItem> historyList = mDatabaseHandler.findItemsContaining(constraint.toString());
synchronized (mHistory) {
mHistory.clear();
@@ -261,7 +254,7 @@ public class SearchAdapter extends BaseAdapter implements Filterable {
}
@Override
public CharSequence convertResultToString(Object resultValue) {
public CharSequence convertResultToString(@NonNull Object resultValue) {
return ((HistoryItem) resultValue).getUrl();
}
@@ -270,7 +263,7 @@ public class SearchAdapter extends BaseAdapter implements Filterable {
synchronized (mFilteredList) {
mFilteredList.clear();
List<HistoryItem> filtered = getFilteredList();
Collections.sort(filtered, mComparator);
Collections.sort(filtered, sComparator);
mFilteredList.addAll(filtered);
}
notifyDataSetChanged();
@@ -278,142 +271,24 @@ public class SearchAdapter extends BaseAdapter implements Filterable {
}
private static class SuggestionHolder {
ImageView mImage;
TextView mTitle;
TextView mUrl;
@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();
}
}
private class RetrieveSearchSuggestions extends AsyncTask<String, Void, List<HistoryItem>> {
private XmlPullParserFactory mFactory;
private XmlPullParser mXpp;
@Override
protected List<HistoryItem> doInBackground(String... arg0) {
mIsExecuting = true;
List<HistoryItem> filter = new ArrayList<>();
String query = arg0[0];
try {
query = SPACE_PATTERN.matcher(query).replaceAll("+");
URLEncoder.encode(query, ENCODING);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
File cache = downloadSuggestionsForQuery(query);
if (!cache.exists()) {
return filter;
}
InputStream fileInput = null;
try {
fileInput = new BufferedInputStream(new FileInputStream(cache));
if (mFactory == null) {
mFactory = XmlPullParserFactory.newInstance();
mFactory.setNamespaceAware(true);
}
if (mXpp == null) {
mXpp = mFactory.newPullParser();
}
mXpp.setInput(fileInput, ENCODING);
int eventType = mXpp.getEventType();
int counter = 0;
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG && "suggestion".equals(mXpp.getName())) {
String suggestion = mXpp.getAttributeValue(null, "data");
filter.add(new HistoryItem(mSearchSubtitle + " \"" + suggestion + '"',
suggestion, R.drawable.ic_search));
counter++;
if (counter >= 5) {
break;
}
}
eventType = mXpp.next();
}
} catch (Exception e) {
return filter;
} finally {
Utils.close(fileInput);
}
return filter;
}
@Override
protected void onPostExecute(List<HistoryItem> result) {
mIsExecuting = false;
synchronized (mSuggestions) {
mSuggestions.clear();
mSuggestions.addAll(result);
}
synchronized (mFilteredList) {
mFilteredList.clear();
List<HistoryItem> filtered = getFilteredList();
Collections.sort(filtered, mComparator);
mFilteredList.addAll(filtered);
notifyDataSetChanged();
}
}
}
/**
* 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
*/
private static File downloadSuggestionsForQuery(String query) {
File cacheFile = new File(BrowserApp.getAppContext().getCacheDir(), query.hashCode() + CACHE_FILE_TYPE);
if (System.currentTimeMillis() - INTERVAL_DAY < cacheFile.lastModified()) {
return cacheFile;
}
if (!isNetworkConnected(BrowserApp.getAppContext())) {
return cacheFile;
}
InputStream in = null;
FileOutputStream fos = null;
try {
URL url = new URL("http://google.com/complete/search?q=" + query
+ "&output=toolbar&hl=en");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.connect();
in = connection.getInputStream();
if (in != null) {
fos = new FileOutputStream(cacheFile);
int buffer;
while ((buffer = in.read()) != -1) {
fos.write(buffer);
}
fos.flush();
}
cacheFile.setLastModified(System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
} finally {
Utils.close(in);
Utils.close(fos);
}
return cacheFile;
}
private static boolean isNetworkConnected(Context context) {
NetworkInfo networkInfo = getActiveNetworkInfo(context);
return networkInfo != null && networkInfo.isConnected();
}
private static NetworkInfo getActiveNetworkInfo(Context context) {
ConnectivityManager connectivity = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivity == null) {
return null;
}
return connectivity.getActiveNetworkInfo();
}
private List<HistoryItem> getFilteredList() {
@NonNull
private synchronized List<HistoryItem> getFilteredList() {
List<HistoryItem> list = new ArrayList<>(5);
synchronized (mBookmarks) {
synchronized (mHistory) {
@@ -444,7 +319,7 @@ public class SearchAdapter extends BaseAdapter implements Filterable {
private static class SuggestionsComparator implements Comparator<HistoryItem> {
@Override
public int compare(HistoryItem lhs, HistoryItem rhs) {
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;
@@ -0,0 +1,21 @@
package acr.browser.lightning.search;
import android.support.annotation.NonNull;
import java.util.List;
import acr.browser.lightning.database.HistoryItem;
interface SuggestionsResult {
/**
* Called when the search suggestions have
* been retrieved from the server.
*
* @param searchResults the results, a valid
* list of results. May
* be empty.
*/
void resultReceived(@NonNull List<HistoryItem> searchResults);
}
@@ -2,6 +2,8 @@ package acr.browser.lightning.utils;
import android.content.Context;
import android.content.res.AssetManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.io.BufferedReader;
@@ -13,9 +15,14 @@ import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.preference.PreferenceManager;
@Singleton
public class AdBlock {
private static final String TAG = "AdBlock";
@@ -31,34 +38,31 @@ public class AdBlock {
private final Set<String> mBlockedDomainsList = new HashSet<>();
private boolean mBlockAds;
private static final Locale mLocale = Locale.getDefault();
private static AdBlock mInstance;
public static AdBlock getInstance(Context context) {
if (mInstance == null) {
mInstance = new AdBlock(context);
}
return mInstance;
}
@Inject PreferenceManager mPreferenceManager;
private AdBlock(Context context) {
@Inject
public AdBlock(@NonNull Context context) {
BrowserApp.getAppComponent().inject(this);
if (mBlockedDomainsList.isEmpty() && Constants.FULL_VERSION) {
loadHostsFile(context);
}
mBlockAds = PreferenceManager.getInstance().getAdBlockEnabled();
mBlockAds = mPreferenceManager.getAdBlockEnabled();
}
public void updatePreference() {
mBlockAds = PreferenceManager.getInstance().getAdBlockEnabled();
mBlockAds = mPreferenceManager.getAdBlockEnabled();
}
private void loadBlockedDomainsList(final Context context) {
Thread thread = new Thread(new Runnable() {
private void loadBlockedDomainsList(@NonNull final Context context) {
BrowserApp.getTaskThread().execute(new Runnable() {
@Override
public void run() {
AssetManager asset = context.getAssets();
BufferedReader reader = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
reader = new BufferedReader(new InputStreamReader(
asset.open(BLOCKED_DOMAINS_LIST_FILE_NAME)));
String line;
@@ -73,16 +77,16 @@ public class AdBlock {
}
}
});
thread.start();
}
/**
* a method that determines if the given URL is an ad or not. It performs
* a search of the URL's domain on the blocked domain hash set.
*
* @param url the URL to check for being an ad
* @return true if it is an ad, false if it is not an ad
*/
public boolean isAd(String url) {
public boolean isAd(@Nullable String url) {
if (!mBlockAds || url == null) {
return false;
}
@@ -104,11 +108,13 @@ public class AdBlock {
/**
* Returns the probable domain name for a given URL
*
* @param url the url to parse
* @return returns the domain
* @throws URISyntaxException throws an exception if the string cannot form a URI
*/
private static String getDomainName(String url) throws URISyntaxException {
@NonNull
private static String getDomainName(@NonNull String url) throws URISyntaxException {
int index = url.indexOf('/', 8);
if (index != -1) {
url = url.substring(0, index);
@@ -129,16 +135,18 @@ public class AdBlock {
* simply have a list of hostnames to block, or it can handle a full blown hosts file.
* It will strip out comments, references to the base IP address and just extract the
* domains to be used
*
* @param context the context needed to read the file
*/
private void loadHostsFile(final Context context) {
Thread thread = new Thread(new Runnable() {
private void loadHostsFile(@NonNull final Context context) {
BrowserApp.getTaskThread().execute(new Runnable() {
@Override
public void run() {
AssetManager asset = context.getAssets();
BufferedReader reader = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
reader = new BufferedReader(new InputStreamReader(
asset.open(BLOCKED_DOMAINS_LIST_FILE_NAME)));
String line;
@@ -172,6 +180,5 @@ public class AdBlock {
}
}
});
thread.start();
}
}
@@ -0,0 +1,74 @@
package acr.browser.lightning.utils;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.Typeface;
public class DrawableUtils {
public static Bitmap getRoundedNumberImage(int number, int width, int height, int color, int thickness) {
String text;
if (number > 99) {
text = "\u221E";
} else {
text = String.valueOf(number);
}
Bitmap image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(image);
Paint paint = new Paint();
paint.setColor(color);
Typeface boldText = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
paint.setTypeface(boldText);
paint.setTextSize(Utils.dpToPx(14));
paint.setAntiAlias(true);
paint.setTextAlign(Paint.Align.CENTER);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
int radius = Utils.dpToPx(2);
RectF outer = new RectF(0, 0, canvas.getWidth(), canvas.getHeight());
canvas.drawRoundRect(outer, radius, radius, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
radius--;
RectF inner = new RectF(thickness, thickness, canvas.getWidth() - thickness, canvas.getHeight() - thickness);
canvas.drawRoundRect(inner, radius, radius, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
int xPos = (canvas.getWidth() / 2);
int yPos = (int) ((canvas.getHeight() / 2) - ((paint.descent() + paint.ascent()) / 2));
canvas.drawText(String.valueOf(text), xPos, yPos, paint);
return image;
}
public static int mixColor(float fraction, int startValue, int endValue) {
int startInt = startValue;
int startA = (startInt >> 24) & 0xff;
int startR = (startInt >> 16) & 0xff;
int startG = (startInt >> 8) & 0xff;
int startB = startInt & 0xff;
int endInt = endValue;
int endA = (endInt >> 24) & 0xff;
int endR = (endInt >> 16) & 0xff;
int endG = (endInt >> 8) & 0xff;
int endB = endInt & 0xff;
return (startA + (int)(fraction * (endA - startA))) << 24 |
(startR + (int)(fraction * (endR - startR))) << 16 |
(startG + (int)(fraction * (endG - startG))) << 8 |
(startB + (int)(fraction * (endB - startB)));
}
}
@@ -0,0 +1,112 @@
package acr.browser.lightning.utils;
import android.app.Application;
import android.os.Bundle;
import android.os.Parcel;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.Constants;
/**
* A utility class containing helpful methods
* pertaining to file storage.
*/
public class FileUtils {
/**
* Writes a bundle to persistent storage in the files directory
* using the specified file name. This method is a blocking
* operation.
*
* @param app the application needed to obtain the file directory.
* @param bundle the bundle to store in persistent storage.
* @param name the name of the file to store the bundle in.
*/
public static void writeBundleToStorage(final @NonNull Application app, final Bundle bundle, final @NonNull String name) {
BrowserApp.getIOThread().execute(new Runnable() {
@Override
public void run() {
File outputFile = new File(app.getFilesDir(), name);
FileOutputStream outputStream = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
outputStream = new FileOutputStream(outputFile);
Parcel parcel = Parcel.obtain();
parcel.writeBundle(bundle);
outputStream.write(parcel.marshall());
outputStream.flush();
parcel.recycle();
} catch (IOException e) {
Log.e(Constants.TAG, "Unable to write bundle to storage");
} finally {
Utils.close(outputStream);
}
}
});
}
/**
* Use this method to delete the bundle with the specified name.
* This is a blocking call and should be used within a worker
* thread unless immediate deletion is necessary.
*
* @param app the application object needed to get the file.
* @param name the name of the file.
*/
public static void deleteBundleInStorage(final @NonNull Application app, final @NonNull String name) {
File outputFile = new File(app.getFilesDir(), name);
if (outputFile.exists()) {
outputFile.delete();
}
}
/**
* Reads a bundle from the file with the specified
* name in the peristent storage files directory.
* This method is a blocking operation.
*
* @param app the application needed to obtain the files directory.
* @param name the name of the file to read from.
* @return a valid Bundle loaded using the system class loader
* or null if the method was unable to read the Bundle from storage.
*/
@Nullable
public static Bundle readBundleFromStorage(@NonNull Application app, @NonNull String name) {
File inputFile = new File(app.getFilesDir(), name);
FileInputStream inputStream = null;
try {
//noinspection IOResourceOpenedButNotSafelyClosed
inputStream = new FileInputStream(inputFile);
Parcel parcel = Parcel.obtain();
byte[] data = new byte[(int) inputStream.getChannel().size()];
//noinspection ResultOfMethodCallIgnored
inputStream.read(data, 0, data.length);
parcel.unmarshall(data, 0, data.length);
parcel.setDataPosition(0);
Bundle out = parcel.readBundle(ClassLoader.getSystemClassLoader());
out.putAll(out);
parcel.recycle();
return out;
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "Unable to read bundle from storage");
} catch (IOException e) {
e.printStackTrace();
} finally {
//noinspection ResultOfMethodCallIgnored
inputFile.delete();
Utils.close(inputStream);
}
return null;
}
}
@@ -7,6 +7,9 @@ import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.webkit.WebView;
@@ -15,6 +18,8 @@ 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;
@@ -23,14 +28,14 @@ public class IntentUtils {
+ // switch on case insensitive matching
'('
+ // begin group for schema
"(?:http|https|file):\\/\\/" + "|(?:inline|data|about|javascript):" + "|(?:.*:.*@)"
"(?:http|https|file)://" + "|(?:inline|data|about|javascript):" + "|(?:.*:.*@)"
+ ')' + "(.*)");
public IntentUtils(Activity activity) {
mActivity = activity;
}
public boolean startActivityForUrl(WebView tab, String url) {
public boolean startActivityForUrl(@Nullable WebView tab, @NonNull String url) {
Intent intent;
try {
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
@@ -39,6 +44,12 @@ public class IntentUtils {
return false;
}
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
intent.setSelector(null);
}
if (mActivity.getPackageManager().resolveActivity(intent, 0) == null) {
String packagename = intent.getPackage();
if (packagename != null) {
@@ -51,10 +62,8 @@ public class IntentUtils {
return false;
}
}
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
if (tab != null) {
intent.putExtra(mActivity.getPackageName() + ".Origin", 1);
intent.putExtra(Constants.INTENT_ORIGIN, 1);
}
Matcher m = ACCEPTED_URI_SCHEMA.matcher(url);
@@ -0,0 +1,67 @@
package acr.browser.lightning.utils;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewTreeObserver;
public class KeyboardHelper {
public interface KeyboardListener {
/**
* Called when the visibility of the keyboard changes.
* Parameter tells whether the keyboard has been shown
* or hidden.
*
* @param visible true if the keyboard has been shown,
* false otherwise.
*/
void keyboardVisibilityChanged(boolean visible);
}
@NonNull private final View mView;
private int mLastRight = -1;
private int mLastBottom = -1;
/**
* Constructor
*
* @param view the view to listen on, should be
* the {@link android.R.id#content} view.
*/
public KeyboardHelper(@NonNull View view) {
mView = view;
}
/**
* Registers a {@link KeyboardListener} to receive
* callbacks when the keyboard is shown for the specific
* view. The view used should be the content view as it
* will receive resize events from the system.
*
* @param listener the listener to register to receive events.
*/
public void registerKeyboardListener(@NonNull final KeyboardListener listener) {
mView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Rect rect = new Rect();
if (mLastBottom == -1) {
mLastBottom = rect.bottom;
}
if (mLastRight == -1) {
mLastRight = rect.right;
}
mView.getWindowVisibleDisplayFrame(rect);
if (mLastRight == rect.right && rect.bottom < mLastBottom) {
listener.keyboardVisibilityChanged(true);
} else if (mLastRight == rect.right && rect.bottom > mLastBottom) {
listener.keyboardVisibilityChanged(false);
}
mLastBottom = rect.bottom;
mLastRight = rect.right;
}
});
}
}
@@ -0,0 +1,80 @@
package acr.browser.lightning.utils;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.inputmethod.InputMethodManager;
import java.lang.reflect.Method;
public class MemoryLeakUtils {
private static final String TAG = MemoryLeakUtils.class.getSimpleName();
private static Method sFinishInputLocked = null;
/**
* Clears the mNextServedView and mServedView in
* InputMethodManager and keeps them from leaking.
*
* @param application the application needed to get
* the InputMethodManager that is
* leaking the views.
*/
public static void clearNextServedView(@NonNull Application application) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
// This shouldn't be a problem on N
return;
}
InputMethodManager imm = (InputMethodManager) application.getSystemService(Context.INPUT_METHOD_SERVICE);
if (sFinishInputLocked == null) {
try {
sFinishInputLocked = InputMethodManager.class.getDeclaredMethod("finishInputLocked");
} catch (NoSuchMethodException e) {
Log.d(TAG, "Unable to find method in clearNextServedView", e);
}
}
if (sFinishInputLocked != null) {
sFinishInputLocked.setAccessible(true);
try {
sFinishInputLocked.invoke(imm);
} catch (Exception e) {
Log.d(TAG, "Unable to invoke method in clearNextServedView", e);
}
}
}
public static abstract class LifecycleAdapter implements Application.ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
@Override
public void onActivityStarted(Activity activity) {}
@Override
public void onActivityResumed(Activity activity) {}
@Override
public void onActivityPaused(Activity activity) {}
@Override
public void onActivityStopped(Activity activity) {}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
@Override
public void onActivityDestroyed(Activity activity) {}
}
}
@@ -1,75 +0,0 @@
package acr.browser.lightning.utils;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.annotation.NonNull;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Copyright 8/22/2015 Anthony Restaino
*/
public class PermissionsManager {
private static PermissionsManager mInstance;
private final Set<String> mPendingRequests = new HashSet<>();
public static PermissionsManager getInstance() {
if (mInstance == null) {
mInstance = new PermissionsManager();
}
return mInstance;
}
public void requestPermissionsIfNecessary(Activity activity, @NonNull String[] permissions) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || activity == null) {
return;
}
List<String> permList = new ArrayList<>();
for (String perm : permissions) {
if (activity.checkSelfPermission(perm) != PackageManager.PERMISSION_GRANTED
&& !mPendingRequests.contains(perm)) {
permList.add(perm);
}
}
if (!permList.isEmpty()) {
String[] permsToRequest = permList.toArray(new String[permList.size()]);
mPendingRequests.addAll(permList);
activity.requestPermissions(permsToRequest, 1);
}
}
public static boolean checkPermission(Activity activity, @NonNull String permission) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
} else if (activity == null) {
return false;
}
return activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
}
public static boolean checkPermissions(Activity activity, @NonNull String[] permissions) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
} else if (activity == null) {
return false;
}
boolean permissionsNecessary = true;
for (String perm : permissions) {
permissionsNecessary &= activity.checkSelfPermission(perm) == PackageManager.PERMISSION_GRANTED;
}
return permissionsNecessary;
}
public void notifyPermissionsChange(String[] permissions) {
for (String perm : permissions) {
mPendingRequests.remove(perm);
}
}
}
@@ -0,0 +1,16 @@
package acr.browser.lightning.utils;
public class Preconditions {
/**
* Ensure that an object is not null
* and throw a RuntimeException if it
* is null.
*
* @param object check nullness on this object.
*/
public static void checkNonNull(Object object) {
if (object == null) {
throw new RuntimeException("Object must not be null");
}
}
}
@@ -1,48 +1,46 @@
package acr.browser.lightning.utils;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import com.squareup.otto.Bus;
import net.i2p.android.ui.I2PAndroidHelper;
import javax.inject.Inject;
import javax.inject.Singleton;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.bus.BrowserEvents;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.preference.PreferenceManager;
import info.guardianproject.netcipher.proxy.OrbotHelper;
import info.guardianproject.netcipher.web.WebkitProxy;
/**
* 6/4/2015 Anthony Restaino
*/
@Singleton
public class ProxyUtils {
// Helper
private final I2PAndroidHelper mI2PHelper;
private static boolean mI2PHelperBound;
private static boolean mI2PProxyInitialized;
private final PreferenceManager mPreferences;
private static ProxyUtils mInstance;
private ProxyUtils(Context context) {
mPreferences = PreferenceManager.getInstance();
mI2PHelper = new I2PAndroidHelper(context.getApplicationContext());
}
@Inject PreferenceManager mPreferences;
@Inject I2PAndroidHelper mI2PHelper;
@Inject Bus mBus;
public static ProxyUtils getInstance() {
if (mInstance == null) {
mInstance = new ProxyUtils(BrowserApp.getAppContext());
}
return mInstance;
@Inject
public ProxyUtils() {
BrowserApp.getAppComponent().inject(this);
}
/*
* If Orbot/Tor or I2P is installed, prompt the user if they want to enable
* proxying for this session
*/
public void checkForProxy(final Activity activity) {
public void checkForProxy(@NonNull final Activity activity) {
boolean useProxy = mPreferences.getUseProxy();
final boolean orbotInstalled = OrbotHelper.isOrbotInstalled(activity);
@@ -105,7 +103,7 @@ public class ProxyUtils {
/*
* Initialize WebKit Proxying
*/
private void initializeProxy(Activity activity) {
private void initializeProxy(@NonNull Activity activity) {
String host;
int port;
@@ -143,13 +141,13 @@ public class ProxyUtils {
}
public boolean isProxyReady(Activity activity) {
public boolean isProxyReady() {
if (mPreferences.getProxyChoice() == Constants.PROXY_I2P) {
if (!mI2PHelper.isI2PAndroidRunning()) {
Utils.showSnackbar(activity, R.string.i2p_not_running);
mBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.i2p_not_running));
return false;
} else if (!mI2PHelper.areTunnelsActive()) {
Utils.showSnackbar(activity, R.string.i2p_tunnels_not_ready);
mBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.i2p_tunnels_not_ready));
return false;
}
}
@@ -157,7 +155,7 @@ public class ProxyUtils {
return true;
}
public void updateProxySettings(Activity activity) {
public void updateProxySettings(@NonNull Activity activity) {
if (mPreferences.getUseProxy()) {
initializeProxy(activity);
} else {
@@ -190,7 +188,7 @@ public class ProxyUtils {
}
}
public static int setProxyChoice(int choice, Activity activity) {
public static int setProxyChoice(int choice, @NonNull Activity activity) {
switch (choice) {
case Constants.PROXY_ORBOT:
if (!OrbotHelper.isOrbotInstalled(activity)) {
@@ -200,7 +198,7 @@ public class ProxyUtils {
break;
case Constants.PROXY_I2P:
I2PAndroidHelper ih = new I2PAndroidHelper(activity.getApplicationContext());
I2PAndroidHelper ih = new I2PAndroidHelper(BrowserApp.get(activity));
if (!ih.isI2PAndroidInstalled()) {
choice = Constants.NO_PROXY;
ih.promptToInstall(activity);
@@ -5,6 +5,7 @@ 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;
@@ -12,9 +13,9 @@ 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.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.TypedValue;
import android.widget.ImageView;
@@ -44,20 +45,28 @@ public class ThemeUtils {
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);
}
public static void themeImageView(ImageView icon, Context context, boolean dark) {
@ColorInt
public static int getIconThemeColor(@NonNull Context context, boolean dark) {
return (dark) ? getIconDarkThemeColor(context) : getIconLightThemeColor(context);
}
public static void themeImageView(@NonNull ImageView icon, @NonNull Context context, boolean dark) {
int color = dark ? getIconDarkThemeColor(context) : getIconLightThemeColor(context);
icon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
}
public static Bitmap getThemedBitmap(Context context, @DrawableRes int res, boolean dark) {
@NonNull
public static Bitmap getThemedBitmap(@NonNull Context context, @DrawableRes int res, boolean dark) {
int color = dark ? getIconDarkThemeColor(context) : getIconLightThemeColor(context);
Bitmap sourceBitmap = BitmapFactory.decodeResource(context.getResources(), res);
Bitmap resultBitmap = Bitmap.createBitmap(sourceBitmap.getWidth(), sourceBitmap.getHeight(), Bitmap.Config.ARGB_8888);
@@ -70,34 +79,27 @@ public class ThemeUtils {
return resultBitmap;
}
@Nullable
@NonNull
public static Drawable getThemedDrawable(@NonNull Context context, @DrawableRes int res, boolean dark) {
int color = dark ? getIconDarkThemeColor(context) : getIconLightThemeColor(context);
final Drawable drawable = ContextCompat.getDrawable(context, res);
if (drawable == null)
return null;
drawable.mutate();
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
return drawable;
}
@Nullable
public static Drawable getLightThemedDrawable(@NonNull Context context, @DrawableRes int res) {
final Drawable drawable = ContextCompat.getDrawable(context, res);
if (drawable == null)
return null;
drawable.mutate();
drawable.setColorFilter(getIconLightThemeColor(context), PorterDuff.Mode.SRC_IN);
return drawable;
}
@NonNull
public static ColorDrawable getSelectedBackground(@NonNull Context context, boolean dark) {
final int color = (dark) ? ContextCompat.getColor(context, R.color.selected_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 getTextColor(Context context) {
public static int getThemedTextHintColor(boolean dark){
return 0x80ffffff & (dark ? Color.WHITE : Color.BLACK);
}
public static int getTextColor(@NonNull Context context) {
return getColor(context, android.R.attr.editTextColor);
}
}
@@ -15,12 +15,19 @@
*/
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
*/
@@ -28,7 +35,7 @@ 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):\\/\\/" +
"(?:http|https|file)://" +
"|(?:inline|data|about|javascript):" +
"|(?:.*:.*@)" +
')' +
@@ -53,7 +60,8 @@ public class UrlUtils {
* @return a stripped url like "www.google.com", or the original string if it could
* not be stripped
*/
public static String stripUrl(String url) {
@Nullable
public static String stripUrl(@Nullable String url) {
if (url == null) return null;
Matcher m = STRIP_URL_PATTERN.matcher(url);
if (m.matches()) {
@@ -74,7 +82,8 @@ public class UrlUtils {
* URL. If false, invalid URLs will return null
* @return Original or modified URL
*/
public static String smartUrlFilter(String url, boolean canBeSearch, String searchUrl) {
@NonNull
public static String smartUrlFilter(@NonNull String url, boolean canBeSearch, String searchUrl) {
String inUrl = url.trim();
boolean hasSpace = inUrl.indexOf(' ') != -1;
Matcher matcher = ACCEPTED_URI_SCHEMA.matcher(inUrl);
@@ -99,11 +108,12 @@ public class UrlUtils {
return URLUtil.composeSearchUrl(inUrl,
searchUrl, QUERY_PLACE_HOLDER);
}
return null;
return "";
}
/* package */
static String fixUrl(String inUrl) {
@NonNull
static String fixUrl(@NonNull String inUrl) {
// FIXME: Converting the url to lower case
// duplicates functionality in smartUrlFilter().
// However, changing all current callers of fixUrl to
@@ -135,7 +145,8 @@ public class UrlUtils {
// Returns the filtered URL. Cannot return null, but can return an empty string
/* package */
static String filteredUrl(String inUrl) {
@Nullable
static String filteredUrl(@Nullable String inUrl) {
if (inUrl == null) {
return "";
}
@@ -145,4 +156,44 @@ public class UrlUtils {
}
return inUrl;
}
/**
* Returns whether the given url is the bookmarks/history page or a normal website
*/
public static boolean isSpecialUrl(@Nullable String url) {
return url != null && url.startsWith(Constants.FILE) &&
(url.endsWith(BookmarkPage.FILENAME) ||
url.endsWith(HistoryPage.FILENAME) ||
url.endsWith(StartPage.FILENAME));
}
/**
* Determines if the url is a url for the bookmark page.
*
* @param url the url to check, may be null.
* @return true if the url is a bookmark url, false otherwise.
*/
public static boolean isBookmarkUrl(@Nullable String url) {
return url != null && url.startsWith(Constants.FILE) && url.endsWith(BookmarkPage.FILENAME);
}
/**
* Determines if the url is a url for the history page.
*
* @param url the url to check, may be null.
* @return true if the url is a history url, false otherwise.
*/
public static boolean isHistoryUrl(@Nullable String url) {
return url != null && url.startsWith(Constants.FILE) && url.endsWith(HistoryPage.FILENAME);
}
/**
* Determines if the url is a url for the start page.
*
* @param url the url to check, may be null.
* @return true if the url is a start page url, false otherwise.
*/
public static boolean isStartPageUrl(@Nullable String url) {
return url != null && url.startsWith(Constants.FILE) && url.endsWith(StartPage.FILENAME);
}
}
@@ -3,6 +3,7 @@
*/
package acr.browser.lightning.utils;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
@@ -11,6 +12,7 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -18,16 +20,23 @@ import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Shader;
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.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.webkit.URLUtil;
import com.anthonycr.grant.PermissionsManager;
import com.anthonycr.grant.PermissionsResultAction;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
@@ -37,19 +46,59 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import acr.browser.lightning.R;
import acr.browser.lightning.activity.MainActivity;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.database.HistoryItem;
import acr.browser.lightning.download.DownloadHandler;
import acr.browser.lightning.preference.PreferenceManager;
public final class Utils {
public static void downloadFile(final Activity activity, final String url,
final String userAgent, final String contentDisposition) {
String fileName = URLUtil.guessFileName(url, null, null);
DownloadHandler.onDownloadStart(activity, url, userAgent, contentDisposition, null
);
Log.i(Constants.TAG, "Downloading" + fileName);
public static boolean doesSupportHeaders() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
}
/**
* Downloads a file from the specified URL. Handles permissions
* requests, and creates all the necessary dialogs that must be
* showed to the user.
*
* @param activity activity needed to created dialogs.
* @param url url to download from.
* @param userAgent the user agent of the browser.
* @param contentDisposition the content description of the file.
*/
public static void downloadFile(final Activity activity, final PreferenceManager manager, final String url,
final String userAgent, final String contentDisposition) {
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(activity, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE}, new PermissionsResultAction() {
@Override
public void onGranted() {
String fileName = URLUtil.guessFileName(url, null, null);
DownloadHandler.onDownloadStart(activity, manager, url, userAgent, contentDisposition, null);
Log.i(Constants.TAG, "Downloading" + fileName);
}
@Override
public void onDenied(String permission) {
// TODO Show Message
}
});
}
/**
* Creates a new intent that can launch the email
* app with a subject, address, body, and cc. It
* is used to handle mail:to links.
*
* @param address the address to send the email to.
* @param subject the subject of the email.
* @param body the body of the email.
* @param cc extra addresses to CC.
* @return a valid intent.
*/
@NonNull
public static Intent newEmailIntent(String address, String subject,
String body, String cc) {
Intent intent = new Intent(Intent.ACTION_SEND);
@@ -61,12 +110,19 @@ public final class Utils {
return intent;
}
public static void createInformativeDialog(Context context, @StringRes int title, @StringRes int message) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
/**
* Creates a dialog with only a title, message, and okay button.
*
* @param activity the activity needed to create a dialog.
* @param title the title of the dialog.
* @param message the message of the dialog.
*/
public static void createInformativeDialog(@NonNull Activity activity, @StringRes int title, @StringRes int message) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(title);
builder.setMessage(message)
.setCancelable(true)
.setPositiveButton(context.getResources().getString(R.string.action_ok),
.setPositiveButton(activity.getResources().getString(R.string.action_ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
@@ -76,30 +132,51 @@ public final class Utils {
alert.show();
}
/**
* Displays a snackbar to the user with a String resource.
*
* @param activity the activity needed to create a snackbar.
* @param resource the string resource to show to the user.
*/
public static void showSnackbar(@NonNull Activity activity, @StringRes int resource) {
View view = activity.findViewById(android.R.id.content);
if (view == null) return;
Snackbar.make(view, resource, Snackbar.LENGTH_SHORT).show();
}
public static void showSnackbar(@NonNull Activity activity, String message) {
/**
* Displays a snackbar to the user with a string message.
*
* @param activity the activity needed to create a snackbar.
* @param message the string message to show to the user.
*/
public static void showSnackbar(@NonNull Activity activity, @NonNull String message) {
View view = activity.findViewById(android.R.id.content);
if (view == null) return;
Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
}
/**
* Converts Density Pixels (DP) to Pixels (PX)
* Converts Density Pixels (DP) to Pixels (PX).
*
* @param dp the number of density pixels to convert
* @return the number of pixels
* @param dp the number of density pixels to convert.
* @return the number of pixels that the conversion generates.
*/
public static int dpToPx(int dp) {
public static int dpToPx(float dp) {
DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
return (int) (dp * metrics.density + 0.5f);
}
public static String getDomainName(String url) {
/**
* Extracts the domain name from a URL.
*
* @param url the URL to extract the domain from.
* @return the domain name, or the URL if the domain
* could not be extracted. The domain name may include
* HTTPS if the URL is an SSL supported URL.
*/
@Nullable
public static String getDomainName(@Nullable String url) {
if (url == null || url.isEmpty()) return "";
boolean ssl = url.startsWith(Constants.HTTPS);
@@ -127,16 +204,11 @@ public final class Utils {
return domain.startsWith("www.") ? domain.substring(4) : domain;
}
public static String getProtocol(String url) {
int index = url.indexOf('/');
return url.substring(0, index + 2);
}
public static String[] getArray(String input) {
public static String[] getArray(@NonNull String input) {
return input.split(Constants.SEPARATOR);
}
public static void trimCache(Context context) {
public static void trimCache(@NonNull Context context) {
try {
File dir = context.getCacheDir();
@@ -148,7 +220,7 @@ public final class Utils {
}
}
private static boolean deleteDir(File dir) {
private static boolean deleteDir(@Nullable File dir) {
if (dir != null && dir.isDirectory()) {
String[] children = dir.list();
for (String aChildren : children) {
@@ -169,7 +241,7 @@ public final class Utils {
* @param bitmap is the bitmap to pad.
* @return the padded bitmap.
*/
public static Bitmap padFavicon(Bitmap bitmap) {
public static Bitmap padFavicon(@NonNull Bitmap bitmap) {
int padding = Utils.dpToPx(4);
Bitmap paddedBitmap = Bitmap.createBitmap(bitmap.getWidth() + padding, bitmap.getHeight()
@@ -230,7 +302,7 @@ public final class Utils {
* @param context the context needed to obtain the PackageManager
* @return true if flash is installed, false otherwise
*/
public static boolean isFlashInstalled(Context context) {
public static boolean isFlashInstalled(@NonNull Context context) {
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo ai = pm.getApplicationInfo("com.adobe.flashplayer", 0);
@@ -249,7 +321,7 @@ public final class Utils {
*
* @param closeable the object to close
*/
public static void close(Closeable closeable) {
public static void close(@Nullable Closeable closeable) {
if (closeable == null)
return;
try {
@@ -259,6 +331,24 @@ public final class Utils {
}
}
/**
* Utility method to close cursors. Cursor did not
* implement Closeable until API 16, so using this
* method for when we want to close a cursor.
*
* @param cursor the cursor to close
*/
public static void close(@Nullable Cursor cursor) {
if (cursor == null) {
return;
}
try {
cursor.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Draws the trapezoid background for the horizontal tabs on a canvas object using
* the specified color.
@@ -266,7 +356,7 @@ public final class Utils {
* @param canvas the canvas to draw upon
* @param color the color to use to draw the tab
*/
public static void drawTrapezoid(Canvas canvas, int color, boolean withShader) {
public static void drawTrapezoid(@NonNull Canvas canvas, int color, boolean withShader) {
Paint paint = new Paint();
paint.setColor(color);
@@ -297,4 +387,34 @@ public final class Utils {
canvas.drawPath(wallpath, paint);
}
/**
* Creates a shortcut on the homescreen using the
* {@link HistoryItem} information that opens the
* browser. The icon, URL, and title are used in
* the creation of the shortcut.
*
* @param activity the activity needed to create
* the intent and show a snackbar message
* @param item the HistoryItem to create the shortcut from
*/
public static void createShortcut(@NonNull Activity activity, @NonNull HistoryItem item) {
if (TextUtils.isEmpty(item.getUrl())) {
return;
}
Log.d(Constants.TAG, "Creating shortcut: " + item.getTitle() + ' ' + item.getUrl());
Intent shortcutIntent = new Intent(activity, MainActivity.class);
shortcutIntent.setData(Uri.parse(item.getUrl()));
final String title = TextUtils.isEmpty(item.getTitle()) ? activity.getString(R.string.untitled) : item.getTitle();
Intent addIntent = new Intent();
addIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, title);
addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON, item.getBitmap());
addIntent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
activity.sendBroadcast(addIntent);
Utils.showSnackbar(activity, R.string.message_added_to_homescreen);
}
}
@@ -3,6 +3,7 @@ package acr.browser.lightning.utils;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.WebIconDatabase;
@@ -33,8 +34,8 @@ public class WebUtils {
WebStorage.getInstance().deleteAllData();
}
public static void clearHistory(@NonNull Context context) {
HistoryDatabase.getInstance().deleteHistory();
public static void clearHistory(@NonNull Context context, @NonNull HistoryDatabase historyDatabase) {
historyDatabase.deleteHistory();
WebViewDatabase m = WebViewDatabase.getInstance(context);
m.clearFormData();
m.clearHttpAuthUsernamePassword();
@@ -47,7 +48,7 @@ public class WebUtils {
Utils.trimCache(context);
}
public static void clearCache(WebView view) {
public static void clearCache(@Nullable WebView view) {
if (view == null) return;
view.clearCache(true);
}
@@ -8,6 +8,7 @@ import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.animation.Animation;
@@ -39,12 +40,12 @@ public class AnimatedProgressBar extends LinearLayout {
private int mDrawWidth = 0;
private int mProgressColor;
public AnimatedProgressBar(Context context, AttributeSet attrs) {
public AnimatedProgressBar(@NonNull Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public AnimatedProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
public AnimatedProgressBar(@NonNull Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
@@ -55,7 +56,7 @@ public class AnimatedProgressBar extends LinearLayout {
* @param context is the context passed by the constructor
* @param attrs is the attribute set passed by the constructor
*/
private void init(final Context context, AttributeSet attrs) {
private void init(@NonNull final Context context, AttributeSet attrs) {
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AnimatedProgressBar, 0, 0);
int backgroundColor;
try { // Retrieve the style of the progress bar that the user hopefully set
@@ -90,7 +91,7 @@ public class AnimatedProgressBar extends LinearLayout {
private final Rect mRect = new Rect();
@Override
protected void onDraw(Canvas canvas) {
protected void onDraw(@NonNull Canvas canvas) {
mPaint.setColor(mProgressColor);
mPaint.setStrokeWidth(10);
mRect.right = mRect.left + mDrawWidth;
@@ -209,6 +210,7 @@ public class AnimatedProgressBar extends LinearLayout {
super.onRestoreInstanceState(state);
}
@NonNull
@Override
protected Parcelable onSaveInstanceState() {
@@ -0,0 +1,42 @@
package acr.browser.lightning.view;
import android.app.Application;
import android.graphics.Bitmap;
import android.net.Uri;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.utils.Utils;
class IconCacheTask implements Runnable {
private final Uri uri;
private final Bitmap icon;
private final Application app;
public IconCacheTask(final Uri uri, final Bitmap icon, final Application app) {
this.uri = uri;
this.icon = icon;
this.app = app;
}
@Override
public void run() {
String hash = String.valueOf(uri.getHost().hashCode());
Log.d(Constants.TAG, "Caching icon for " + uri.getHost());
FileOutputStream fos = null;
try {
File image = new File(app.getCacheDir(), hash + ".png");
fos = new FileOutputStream(image);
icon.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
Utils.close(fos);
}
}
}
@@ -0,0 +1,208 @@
package acr.browser.lightning.view;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import android.webkit.GeolocationPermissions;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import com.anthonycr.grant.PermissionsManager;
import com.anthonycr.grant.PermissionsResultAction;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.controller.UIController;
import acr.browser.lightning.utils.Preconditions;
class LightningChromeClient extends WebChromeClient {
private static final String TAG = LightningChromeClient.class.getSimpleName();
private static final String[] PERMISSIONS = new String[]{Manifest.permission.ACCESS_FINE_LOCATION};
@NonNull private final Activity mActivity;
@NonNull private final LightningView mLightningView;
@NonNull private final UIController mUIController;
LightningChromeClient(@NonNull Activity activity, @NonNull LightningView lightningView) {
Preconditions.checkNonNull(activity);
Preconditions.checkNonNull(lightningView);
mActivity = activity;
mUIController = (UIController) activity;
mLightningView = lightningView;
}
@Override
public void onProgressChanged(WebView view, int newProgress) {
if (mLightningView.isShown()) {
mUIController.updateProgress(newProgress);
}
}
@Override
public void onReceivedIcon(@NonNull WebView view, Bitmap icon) {
mLightningView.getTitleInfo().setFavicon(icon);
mUIController.tabChanged(mLightningView);
cacheFavicon(view.getUrl(), icon, mActivity);
}
/**
* Naive caching of the favicon according to the domain name of the URL
*
* @param icon the icon to cache
*/
private static void cacheFavicon(@Nullable final String url, @Nullable final Bitmap icon, @NonNull final Context context) {
if (icon == null || url == null) return;
final Uri uri = Uri.parse(url);
if (uri.getHost() == null) {
return;
}
BrowserApp.getIOThread().execute(new IconCacheTask(uri, icon, BrowserApp.get(context)));
}
@Override
public void onReceivedTitle(@Nullable WebView view, @Nullable String title) {
if (title != null && !title.isEmpty()) {
mLightningView.getTitleInfo().setTitle(title);
} else {
mLightningView.getTitleInfo().setTitle(mActivity.getString(R.string.untitled));
}
mUIController.tabChanged(mLightningView);
if (view != null && view.getUrl() != null) {
mUIController.updateHistory(title, view.getUrl());
}
}
@Override
public void onGeolocationPermissionsShowPrompt(@NonNull final String origin,
@NonNull final GeolocationPermissions.Callback callback) {
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(mActivity, PERMISSIONS, new PermissionsResultAction() {
@Override
public void onGranted() {
final boolean remember = true;
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle(mActivity.getString(R.string.location));
String org;
if (origin.length() > 50) {
org = origin.subSequence(0, 50) + "...";
} else {
org = origin;
}
builder.setMessage(org + mActivity.getString(R.string.message_location))
.setCancelable(true)
.setPositiveButton(mActivity.getString(R.string.action_allow),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
callback.invoke(origin, true, remember);
}
})
.setNegativeButton(mActivity.getString(R.string.action_dont_allow),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
callback.invoke(origin, false, remember);
}
});
AlertDialog alert = builder.create();
alert.show();
}
@Override
public void onDenied(String permission) {
//TODO show message and/or turn off setting
}
});
}
@Override
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture,
Message resultMsg) {
mUIController.onCreateWindow(resultMsg);
return true;
}
@Override
public void onCloseWindow(WebView window) {
mUIController.onCloseWindow(mLightningView);
}
@SuppressWarnings("unused")
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
mUIController.openFileChooser(uploadMsg);
}
@SuppressWarnings("unused")
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
mUIController.openFileChooser(uploadMsg);
}
@SuppressWarnings("unused")
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
mUIController.openFileChooser(uploadMsg);
}
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
WebChromeClient.FileChooserParams fileChooserParams) {
mUIController.showFileChooser(filePathCallback);
return true;
}
/**
* Obtain an image that is displayed as a placeholder on a video until the video has initialized
* and can begin loading.
*
* @return a Bitmap that can be used as a place holder for videos.
*/
@Nullable
@Override
public Bitmap getDefaultVideoPoster() {
final Resources resources = mActivity.getResources();
return BitmapFactory.decodeResource(resources, android.R.drawable.spinner_background);
}
/**
* Inflate a view to send to a LightningView when it needs to display a video and has to
* show a loading dialog. Inflates a progress view and returns it.
*
* @return A view that should be used to display the state
* of a video's loading progress.
*/
@Override
public View getVideoLoadingProgressView() {
LayoutInflater inflater = LayoutInflater.from(mActivity);
return inflater.inflate(R.layout.video_loading_progress, null);
}
@Override
public void onHideCustomView() {
mUIController.onHideCustomView();
}
@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
mUIController.onShowCustomView(view, callback);
}
@SuppressWarnings("deprecation")
@Override
public void onShowCustomView(View view, int requestedOrientation,
CustomViewCallback callback) {
mUIController.onShowCustomView(view, callback, requestedOrientation);
}
}
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
@@ -0,0 +1,106 @@
package acr.browser.lightning.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import acr.browser.lightning.R;
import acr.browser.lightning.utils.ThemeUtils;
import acr.browser.lightning.utils.Utils;
/**
* {@link LightningViewTitle} acts as a container class
* for the favicon and page title, the information used
* by the tab adapters to show the tabs to the user.
*/
class LightningViewTitle {
@Nullable private static Bitmap DEFAULT_DARK_ICON;
@Nullable private static Bitmap DEFAULT_LIGHT_ICON;
@Nullable private Bitmap mFavicon = null;
@NonNull private String mTitle;
@NonNull private final Context mContext;
public LightningViewTitle(@NonNull Context context) {
mContext = context;
mTitle = context.getString(R.string.action_new_tab);
}
/**
* Set the current favicon to a new Bitmap.
* May be null, if null, the default will be used.
*
* @param favicon the potentially null favicon to set.
*/
public void setFavicon(@Nullable Bitmap favicon) {
if (favicon == null) {
mFavicon = null;
} else {
mFavicon = Utils.padFavicon(favicon);
}
}
/**
* Helper method to initialize the DEFAULT_ICON variables
*
* @param context the context needed to initialize the Bitmap.
* @param darkTheme whether the icon should be themed dark or not.
* @return a not null icon.
*/
@NonNull
private static Bitmap getDefaultIcon(@NonNull Context context, boolean darkTheme) {
if (darkTheme) {
if (DEFAULT_DARK_ICON == null) {
DEFAULT_DARK_ICON = ThemeUtils.getThemedBitmap(context, R.drawable.ic_webpage, true);
}
return DEFAULT_DARK_ICON;
} else {
if (DEFAULT_LIGHT_ICON == null) {
DEFAULT_LIGHT_ICON = ThemeUtils.getThemedBitmap(context, R.drawable.ic_webpage, false);
}
return DEFAULT_LIGHT_ICON;
}
}
/**
* Set the current title to a new title.
* Must not be null.
*
* @param title the non-null title to set.
*/
public void setTitle(@Nullable String title) {
if (title == null) {
mTitle = "";
} else {
mTitle = title;
}
}
/**
* Gets the current title, which is not null.
* Can be an empty string.
*
* @return the non-null title.
*/
@NonNull
public String getTitle() {
return mTitle;
}
/**
* Gets the favicon of the page, which is not null.
* Either the favicon, or a default icon.
*
* @return the favicon or a default if that is null.
*/
@NonNull
public Bitmap getFavicon(boolean darkTheme) {
if (mFavicon == null) {
return getDefaultIcon(mContext, darkTheme);
}
return mFavicon;
}
}
@@ -0,0 +1,325 @@
package acr.browser.lightning.view;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.MailTo;
import android.net.http.SslError;
import android.os.Build;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.text.InputType;
import android.text.method.PasswordTransformationMethod;
import android.util.Log;
import android.webkit.HttpAuthHandler;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.EditText;
import android.widget.LinearLayout;
import java.io.ByteArrayInputStream;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import acr.browser.lightning.R;
import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.controller.UIController;
import acr.browser.lightning.utils.AdBlock;
import acr.browser.lightning.utils.IntentUtils;
import acr.browser.lightning.utils.Preconditions;
import acr.browser.lightning.utils.ProxyUtils;
import acr.browser.lightning.utils.Utils;
public class LightningWebClient extends WebViewClient {
@NonNull private final Activity mActivity;
@NonNull private final LightningView mLightningView;
@NonNull private final UIController mUIController;
@NonNull private final IntentUtils mIntentUtils;
@Inject ProxyUtils mProxyUtils;
@Inject AdBlock mAdBlock;
LightningWebClient(@NonNull Activity activity, @NonNull LightningView lightningView) {
BrowserApp.getAppComponent().inject(this);
Preconditions.checkNonNull(activity);
Preconditions.checkNonNull(lightningView);
mActivity = activity;
mUIController = (UIController) activity;
mLightningView = lightningView;
mAdBlock.updatePreference();
mIntentUtils = new IntentUtils(activity);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, @NonNull WebResourceRequest request) {
if (mAdBlock.isAd(request.getUrl().toString())) {
ByteArrayInputStream EMPTY = new ByteArrayInputStream("".getBytes());
return new WebResourceResponse("text/plain", "utf-8", EMPTY);
}
return super.shouldInterceptRequest(view, request);
}
@Nullable
@SuppressWarnings("deprecation")
@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (mAdBlock.isAd(url)) {
ByteArrayInputStream EMPTY = new ByteArrayInputStream("".getBytes());
return new WebResourceResponse("text/plain", "utf-8", EMPTY);
}
return null;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void onPageFinished(@NonNull WebView view, String url) {
if (view.isShown()) {
mUIController.updateUrl(url, true);
mUIController.setBackButtonEnabled(view.canGoBack());
mUIController.setForwardButtonEnabled(view.canGoForward());
view.postInvalidate();
}
if (view.getTitle() == null || view.getTitle().isEmpty()) {
mLightningView.getTitleInfo().setTitle(mActivity.getString(R.string.untitled));
} else {
mLightningView.getTitleInfo().setTitle(view.getTitle());
}
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT &&
mLightningView.getInvertePage()) {
view.evaluateJavascript(Constants.JAVASCRIPT_INVERT_PAGE, null);
}
mUIController.tabChanged(mLightningView);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
mLightningView.getTitleInfo().setFavicon(null);
if (mLightningView.isShown()) {
mUIController.updateUrl(url, false);
mUIController.showActionBar();
}
mUIController.tabChanged(mLightningView);
}
@Override
public void onReceivedHttpAuthRequest(final WebView view, @NonNull final HttpAuthHandler handler,
final String host, final String realm) {
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
final EditText name = new EditText(mActivity);
final EditText password = new EditText(mActivity);
LinearLayout passLayout = new LinearLayout(mActivity);
passLayout.setOrientation(LinearLayout.VERTICAL);
passLayout.addView(name);
passLayout.addView(password);
name.setHint(mActivity.getString(R.string.hint_username));
name.setSingleLine();
password.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
password.setSingleLine();
password.setTransformationMethod(new PasswordTransformationMethod());
password.setHint(mActivity.getString(R.string.hint_password));
builder.setTitle(mActivity.getString(R.string.title_sign_in));
builder.setView(passLayout);
builder.setCancelable(true)
.setPositiveButton(mActivity.getString(R.string.title_sign_in),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
String user = name.getText().toString();
String pass = password.getText().toString();
handler.proceed(user.trim(), pass.trim());
Log.d(Constants.TAG, "Request Login");
}
})
.setNegativeButton(mActivity.getString(R.string.action_cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
handler.cancel();
}
});
AlertDialog alert = builder.create();
alert.show();
}
private boolean mIsRunning = false;
private float mZoomScale = 0.0f;
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void onScaleChanged(@NonNull final WebView view, final float oldScale, final float newScale) {
if (view.isShown() && mLightningView.mPreferences.getTextReflowEnabled() &&
Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
if (mIsRunning)
return;
if (Math.abs(mZoomScale - newScale) > 0.01f) {
mIsRunning = view.postDelayed(new Runnable() {
@Override
public void run() {
mZoomScale = newScale;
view.evaluateJavascript(Constants.JAVASCRIPT_TEXT_REFLOW, null);
mIsRunning = false;
}
}, 100);
}
}
}
@NonNull
private static List<Integer> getAllSslErrorMessageCodes(@NonNull SslError error) {
List<Integer> errorCodeMessageCodes = new ArrayList<>(1);
if (error.hasError(SslError.SSL_DATE_INVALID)) {
errorCodeMessageCodes.add(R.string.message_certificate_date_invalid);
}
if (error.hasError(SslError.SSL_EXPIRED)) {
errorCodeMessageCodes.add(R.string.message_certificate_expired);
}
if (error.hasError(SslError.SSL_IDMISMATCH)) {
errorCodeMessageCodes.add(R.string.message_certificate_domain_mismatch);
}
if (error.hasError(SslError.SSL_NOTYETVALID)) {
errorCodeMessageCodes.add(R.string.message_certificate_not_yet_valid);
}
if (error.hasError(SslError.SSL_UNTRUSTED)) {
errorCodeMessageCodes.add(R.string.message_certificate_untrusted);
}
if (error.hasError(SslError.SSL_INVALID)) {
errorCodeMessageCodes.add(R.string.message_certificate_invalid);
}
return errorCodeMessageCodes;
}
@Override
public void onReceivedSslError(WebView view, @NonNull final SslErrorHandler handler, @NonNull SslError error) {
List<Integer> errorCodeMessageCodes = getAllSslErrorMessageCodes(error);
StringBuilder stringBuilder = new StringBuilder();
for (Integer messageCode : errorCodeMessageCodes) {
stringBuilder.append(" - ").append(mActivity.getString(messageCode)).append('\n');
}
String alertMessage =
mActivity.getString(R.string.message_insecure_connection, stringBuilder.toString());
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle(mActivity.getString(R.string.title_warning));
builder.setMessage(alertMessage)
.setCancelable(true)
.setPositiveButton(mActivity.getString(R.string.action_yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
handler.proceed();
}
})
.setNegativeButton(mActivity.getString(R.string.action_no),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
handler.cancel();
}
});
builder.create().show();
}
@Override
public void onFormResubmission(WebView view, @NonNull final Message dontResend, @NonNull final Message resend) {
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle(mActivity.getString(R.string.title_form_resubmission));
builder.setMessage(mActivity.getString(R.string.message_form_resubmission))
.setCancelable(true)
.setPositiveButton(mActivity.getString(R.string.action_yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
resend.sendToTarget();
}
})
.setNegativeButton(mActivity.getString(R.string.action_no),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dontResend.sendToTarget();
}
});
AlertDialog alert = builder.create();
alert.show();
}
@Override
public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull String url) {
// Check if configured proxy is available
if (!mProxyUtils.isProxyReady()) {
// User has been notified
return true;
}
Map<String, String> headers = mLightningView.getRequestHeaders();
if (mLightningView.isIncognito() && Utils.doesSupportHeaders()) {
view.loadUrl(url, headers);
return true;
}
if (url.startsWith("about:") && Utils.doesSupportHeaders()) {
view.loadUrl(url, headers);
return true;
}
if (url.startsWith("mailto:")) {
MailTo mailTo = MailTo.parse(url);
Intent i = Utils.newEmailIntent(mailTo.getTo(), mailTo.getSubject(),
mailTo.getBody(), mailTo.getCc());
mActivity.startActivity(i);
view.reload();
return true;
} else if (url.startsWith("intent://")) {
Intent intent;
try {
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
} catch (URISyntaxException ignored) {
intent = null;
}
if (intent != null) {
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
intent.setSelector(null);
}
try {
mActivity.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.e(Constants.TAG, "ActivityNotFoundException");
}
return true;
}
}
if (!mIntentUtils.startActivityForUrl(view, url) && Utils.doesSupportHeaders()) {
view.loadUrl(url, headers);
}
return Utils.doesSupportHeaders() || super.shouldOverrideUrlLoading(view, url);
}
}
@@ -0,0 +1,62 @@
package acr.browser.lightning.view;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.AutoCompleteTextView;
public class SearchView extends AutoCompleteTextView {
public interface PreFocusListener {
void onPreFocus();
}
@Nullable private PreFocusListener mListener;
private boolean mIsBeingClicked;
private long mTimePressed;
public SearchView(Context context) {
super(context);
}
public SearchView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SearchView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setOnPreFocusListener(@Nullable PreFocusListener listener) {
mListener = listener;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mTimePressed = System.currentTimeMillis();
mIsBeingClicked = true;
break;
case MotionEvent.ACTION_CANCEL:
mIsBeingClicked = false;
break;
case MotionEvent.ACTION_UP:
if (mIsBeingClicked && !isLongPress()) {
if (mListener != null) {
mListener.onPreFocus();
}
}
break;
}
return super.onTouchEvent(event);
}
private boolean isLongPress() {
return (System.currentTimeMillis() - mTimePressed) >= ViewConfiguration.getLongPressTimeout();
}
}
Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 156 B

+19 -17
Ver Arquivo
@@ -1,6 +1,7 @@
<!-- Copyright 2014 ACR Development -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout
android:id="@+id/main_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@null"
@@ -18,30 +19,31 @@
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar" />
<include layout="@layout/toolbar"/>
<include layout="@layout/browser_content" />
<include layout="@layout/browser_content"/>
<include layout="@layout/search_interface" />
<include layout="@layout/search_interface"/>
</LinearLayout>
<include layout="@layout/tab_drawer" />
<FrameLayout
android:id="@+id/left_drawer"
android:layout_width="@dimen/navigation_width"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="?attr/drawerBackground"
android:fitsSystemWindows="true"
android:weightSum="1"/>
<FrameLayout
android:weightSum="1"
android:layout_gravity="end"
android:fitsSystemWindows="true"
android:id="@+id/right_drawer"
android:background="?attr/drawerBackground"
android:layout_width="@dimen/navigation_width"
android:layout_height="match_parent">
<fragment
android:id="@+id/bookmark_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="acr.browser.lightning.fragment.BookmarksFragment" />
</FrameLayout>
<!-- include layout="@layout/bookmark_drawer" / -->
android:layout_height="match_parent"
android:layout_gravity="end"
android:background="?attr/drawerBackground"
android:fitsSystemWindows="true"
android:weightSum="1"/>
</android.support.v4.widget.DrawerLayout>
</LinearLayout>
+9 -7
Ver Arquivo
@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/drawerBackground"
android:clickable="true"
android:orientation="vertical">
@@ -26,7 +28,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="Favicon"
android:src="@drawable/ic_action_star" />
android:src="@drawable/ic_action_star"/>
</FrameLayout>
<TextView
@@ -38,7 +40,7 @@
android:maxLines="1"
android:minHeight="?android:attr/listPreferredItemHeight"
android:text="@string/action_bookmarks"
android:textAppearance="?android:attr/textAppearanceLarge" />
android:textAppearance="?android:attr/textAppearanceLarge"/>
</LinearLayout>
<ListView
@@ -48,7 +50,7 @@
android:layout_weight="1"
android:divider="@null"
android:dividerHeight="0dp"
android:listSelector="?attr/listBackground" />
android:listSelector="?attr/listBackground"/>
<LinearLayout
android:layout_width="match_parent"
@@ -76,7 +78,7 @@
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingTop="4dp"
android:src="@drawable/ic_action_desktop" />
android:src="@drawable/ic_action_desktop"/>
</FrameLayout>
<FrameLayout
@@ -97,7 +99,7 @@
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingTop="4dp"
android:src="@drawable/ic_action_star" />
android:src="@drawable/ic_action_star"/>
</FrameLayout>
<FrameLayout
@@ -118,7 +120,7 @@
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingTop="4dp"
android:src="@drawable/ic_action_reading" />
android:src="@drawable/ic_action_reading"/>
</FrameLayout>
</LinearLayout>
</LinearLayout>
+4 -2
Ver Arquivo
@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout
android:id="@+id/content_frame"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@color/primary_color" />
android:background="@color/primary_color"
android:clipChildren="true"/>
+1 -2
Ver Arquivo
@@ -16,7 +16,7 @@
android:focusableInTouchMode="true"
android:gravity="center">
<AutoCompleteTextView
<acr.browser.lightning.view.SearchView
android:id="@+id/search"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -29,7 +29,6 @@
android:paddingLeft="8dp"
android:paddingRight="5dp"
android:paddingTop="1dp"
android:selectAllOnFocus="true"
android:singleLine="true"
android:textColor="@color/gray_dark"
android:textColorHint="@color/hint_text"
+2 -2
Ver Arquivo
@@ -30,7 +30,7 @@
android:orientation="horizontal">
<ImageButton
android:id="@+id/button_next"
android:id="@+id/button_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:contentDescription="@null"
@@ -44,7 +44,7 @@
android:background="?android:attr/dividerVertical" />
<ImageButton
android:id="@+id/button_back"
android:id="@+id/button_next"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:contentDescription="@null"
+28 -29
Ver Arquivo
@@ -1,12 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/left_drawer"
android:layout_width="@dimen/navigation_width"
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="?attr/drawerBackground"
android:clickable="true"
android:fitsSystemWindows="true"
android:orientation="vertical">
<LinearLayout
@@ -29,7 +27,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="@string/action_new_tab"
android:src="@drawable/ic_action_tabs" />
android:src="@drawable/ic_action_tabs"/>
</FrameLayout>
<TextView
@@ -41,16 +39,17 @@
android:maxLines="1"
android:minHeight="?android:attr/listPreferredItemHeight"
android:text="@string/tabs"
android:textAppearance="?android:attr/textAppearanceLarge" />
android:textAppearance="?android:attr/textAppearanceLarge"/>
</LinearLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/left_drawer_list"
android:id="@+id/tabs_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:divider="@null"
android:dividerHeight="0dp" />
android:dividerHeight="0dp"
android:overScrollMode="ifContentScrolls"/>
<LinearLayout
android:layout_width="match_parent"
@@ -74,24 +73,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="@string/action_back"
android:src="@drawable/ic_action_back" />
</FrameLayout>
<FrameLayout
android:id="@+id/new_tab_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?attr/actionBarItemBackground"
android:clickable="true">
<ImageView
android:id="@+id/icon_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="@string/action_new_tab"
android:src="@drawable/ic_action_plus" />
android:src="@drawable/ic_action_back"/>
</FrameLayout>
<FrameLayout
@@ -108,7 +90,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="@string/action_homepage"
android:src="@drawable/ic_action_home" />
android:src="@drawable/ic_action_home"/>
</FrameLayout>
<FrameLayout
@@ -125,7 +107,24 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="@string/action_forward"
android:src="@drawable/ic_action_forward" />
android:src="@drawable/ic_action_forward"/>
</FrameLayout>
<FrameLayout
android:id="@+id/new_tab_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?attr/actionBarItemBackground"
android:clickable="true">
<ImageView
android:id="@+id/icon_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="@string/action_new_tab"
android:src="@drawable/ic_action_plus"/>
</FrameLayout>
</LinearLayout>
+46 -54
Ver Arquivo
@@ -1,61 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tab_item_background"
android:layout_width="match_parent"
android:layout_height="56dp"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout android:id="@+id/tab_item_background"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?attr/listBackground"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:weightSum="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/listBackground"
<ImageView
android:id="@+id/faviconTab"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:contentDescription="Favicon"
android:gravity="center_vertical"/>
<TextView
android:id="@+id/textTab"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_weight="1"
android:ellipsize="end"
android:fontFamily="sans-serif-light"
android:gravity="center_vertical"
android:orientation="horizontal"
android:weightSum="1">
android:maxLines="1"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItemSmall"/>
<FrameLayout
android:id="@+id/deleteAction"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:layout_marginRight="4dp"
android:background="?attr/actionBarItemBackground"
android:gravity="center">
<ImageView
android:id="@+id/faviconTab"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:contentDescription="Favicon"
android:gravity="center_vertical" />
<TextView
android:id="@+id/textTab"
android:layout_width="0dp"
android:id="@+id/deleteButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_weight="1"
android:ellipsize="end"
android:fontFamily="sans-serif-light"
android:gravity="center_vertical"
android:maxLines="1"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItemSmall" />
<FrameLayout
android:id="@+id/deleteAction"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:layout_marginRight="4dp"
android:background="?attr/actionBarItemBackground"
android:gravity="center">
<ImageView
android:id="@+id/deleteButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="Delete Tab"
android:src="@drawable/ic_action_delete" />
</FrameLayout>
</LinearLayout>
android:layout_gravity="center"
android:contentDescription="Delete Tab"
android:src="@drawable/ic_action_delete"/>
</FrameLayout>
</LinearLayout>
+26
Ver Arquivo
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="30dp"
android:background="@color/black"
android:orientation="horizontal"
android:weightSum="1">
<android.support.v7.widget.RecyclerView
android:id="@+id/tabs_list"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:fadingEdge="horizontal"
android:overScrollMode="never"
android:scrollbars="none"/>
<ImageView
android:id="@+id/new_tab_button"
android:layout_width="30dp"
android:layout_height="match_parent"
android:background="?actionBarItemBackground"
android:src="@drawable/ic_action_plus"/>
</LinearLayout>

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