├── .gitignore ├── AndroidManifest.template.xml ├── LICENSE ├── README.org ├── assets └── data.json ├── project.clj ├── publish ├── icon_highres.png ├── logo_feature.png ├── play_market_desc.txt ├── shot_grid.png ├── shot_grid_10inch.png ├── shot_grid_7inch.png ├── shot_grid_drawer_open.png ├── shot_login.png ├── shot_problem.png ├── shot_problem_repl_open.png └── shot_problem_solved.png ├── res ├── anim │ ├── slide_in_right.xml │ └── slide_out_left.xml ├── drawable-hdpi │ ├── checkmark_large.png │ ├── ic_checkmark.png │ ├── ic_cross.png │ ├── ic_directions_run_white.png │ ├── ic_exit_to_app_black.png │ ├── ic_format_list_bulleted_white.png │ ├── ic_launcher.png │ ├── ic_menu_friendslist.png │ ├── ic_menu_login.png │ ├── ic_menu_refresh.png │ ├── ic_mode_edit_white.png │ ├── ic_refresh_white.png │ └── ic_sync_white.png ├── drawable-mdpi │ ├── check_icon.png │ ├── ic_directions_run_white.png │ ├── ic_exit_to_app_black.png │ ├── ic_format_list_bulleted_white.png │ ├── ic_launcher.png │ ├── ic_menu_friendslist.png │ ├── ic_menu_login.png │ ├── ic_menu_refresh.png │ ├── ic_mode_edit_white.png │ ├── ic_refresh_white.png │ └── ic_sync_white.png ├── drawable-v21 │ └── card_ripple.xml ├── drawable-xhdpi │ ├── foreclj_logo.png │ ├── ic_directions_run_white.png │ ├── ic_exit_to_app_black.png │ ├── ic_format_list_bulleted_white.png │ ├── ic_launcher.png │ ├── ic_mode_edit_white.png │ ├── ic_refresh_white.png │ └── ic_sync_white.png ├── drawable-xxhdpi │ ├── ic_checkmark_large.png │ ├── ic_directions_run_white.png │ ├── ic_exit_to_app_black.png │ ├── ic_format_list_bulleted_white.png │ ├── ic_launcher.png │ ├── ic_mode_edit_white.png │ ├── ic_refresh_white.png │ └── ic_sync_white.png ├── drawable-xxxhdpi │ ├── ic_directions_run_white.png │ ├── ic_exit_to_app_black.png │ ├── ic_format_list_bulleted_white.png │ ├── ic_launcher.png │ ├── ic_mode_edit_white.png │ ├── ic_refresh_white.png │ └── ic_sync_white.png ├── drawable │ ├── card_ripple.xml │ └── splash_background.xml ├── layout-land │ └── gus.xml ├── layout │ ├── gus.xml │ └── splashscreen.xml └── values │ ├── arrays.xml │ ├── colors.xml │ ├── strings.xml │ └── styles.xml └── src ├── clojure └── org │ └── bytopia │ └── foreclojure │ ├── api.clj │ ├── db.clj │ ├── logic.clj │ ├── main.clj │ ├── problem.clj │ ├── user.clj │ └── utils.clj └── java └── org └── bytopia └── foreclojure ├── CodeboxTextWatcher.java ├── SafeLinkMethod.java └── SplashActivity.java /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /gen 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .encryption-key 12 | -------------------------------------------------------------------------------- /AndroidManifest.template.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {{#debug-build}} 44 | {{/debug-build}} 45 | 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of Washington and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * 4Clojure for Android 2 | 3 | This is the source code for unofficial 4Clojure Android application. The app 4 | is a client for [[http://4clojure.com][4clojure.com]], which is a website for solving Clojure koans 5 | (short programming problems). 4Clojure for Android allows solving these koans 6 | on your mobile phone, without entering the browser, in both online and offline 7 | mode. 8 | 9 | Features so far: 10 | 11 | - Full synchronization with server (new problems and solutions are 12 | downloaded automatically) 13 | - Full offline support (network is not required to solve problems, 14 | solutions can be synchronized later) 15 | - In-client registration 16 | - Acceptable code editor (with highlighting and indentation) 17 | 18 | Plans for future: 19 | - Code golf 20 | - Integration with Clojail 21 | 22 | * How to build 23 | 24 | Set =:sdk-path= in =:android= map in =project.clj=. Execute =lein droid doall= 25 | inside the repository. 26 | 27 | For more information on Clojure-Android initiative check [[http://clojure-android.info/][clojure-android.info]] 28 | and [[https://github.com/clojure-android/lein-droid/wiki/Tutorial][lein-droid tutorial]]. 29 | 30 | * Acknowledgments 31 | 32 | I thank 4clojure.com team for developing and maintaining this great resource, 33 | and for allowing me to base the application upon their work. 34 | 35 | * License 36 | 37 | Copyright © 2015 Alexander Yakushev. Distributed under the Eclipse Public 38 | License, the same as Clojure. See the file [[https://github.com/alexander-yakushev/foreclojure-android/blob/master/LICENSE][LICENSE]]. 39 | 40 | 4clojure.com data (both embedded into app and downloaded later) and logo are 41 | available under the EPL v 1.0 license. 42 | 43 | Green check icon is taken from [[http://iconbug.com/detail/icon/859/green-check/][iconbug.com]] and is provided under CC BY-ND 3.0. 44 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defn get-enc-key [] 2 | (try (binding [*in* (clojure.java.io/reader ".encryption-key")] 3 | (read-line)) 4 | (catch Exception ex 5 | (println (str "WARNING: Couldn't read the encryption key from file. " 6 | "Using empty encryption key.")) 7 | "00000000000000000000000000000000"))) 8 | 9 | (defproject foreclojure-android/foreclojure-android "0.2.0" 10 | :description "Android client for 4clojure.com" 11 | :url "https://github.com/alexander-yakushev/foreclojure-android" 12 | :license {:name "Eclipse Public License" 13 | :url "http://www.eclipse.org/legal/epl-v10.html"} 14 | 15 | :global-vars {*warn-on-reflection* true} 16 | 17 | :source-paths ["src/clojure" "src"] 18 | :java-source-paths ["src/java"] 19 | :javac-options ["-target" "1.6" "-source" "1.6" "-Xlint:-options"] 20 | :plugins [[lein-droid "0.4.0"]] 21 | 22 | :dependencies [[org.clojure-android/clojure "1.7.0-r2"] 23 | [neko/neko "4.0.0-alpha4"] 24 | [org.clojure-android/data.json "0.2.6-SNAPSHOT"] 25 | [com.android.support/design "22.2.1" :extension "aar"]] 26 | :profiles {:default [:dev] 27 | 28 | :dev 29 | [:android-common :android-user 30 | {:dependencies [[org.clojure/tools.nrepl "0.2.10"]] 31 | :target-path "target/debug" 32 | :android {:aot :all-with-unused 33 | :rename-manifest-package "org.bytopia.foreclojure.debug" 34 | :manifest-options {:app-name "4Clojure - debug"}}}] 35 | 36 | :release 37 | [:android-common :android-release 38 | {:target-path "target/release" 39 | :android {:ignore-log-priority [:debug :verbose] 40 | :enable-dynamic-compilation true 41 | :aot :all 42 | :build-type :release}}]} 43 | 44 | :android {:dex-opts ["-JXmx4096M" "--incremental"] 45 | :build-config {"ENC_ALGORITHM" "Blowfish" 46 | "ENC_KEY" #=(get-enc-key)} 47 | :manifest-options {:app-name "@string/app_name"} 48 | :target-version "21" 49 | :aot-exclude-ns ["clojure.parallel" "clojure.core.reducers" 50 | "cljs-tooling.complete" "cljs-tooling.info" 51 | "cljs-tooling.util.analysis" "cljs-tooling.util.misc" 52 | "cider.nrepl" "cider-nrepl.plugin"]}) 53 | -------------------------------------------------------------------------------- /publish/icon_highres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/publish/icon_highres.png -------------------------------------------------------------------------------- /publish/logo_feature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/publish/logo_feature.png -------------------------------------------------------------------------------- /publish/play_market_desc.txt: -------------------------------------------------------------------------------- 1 | Solve 4clojure.com problems on your mobile phone. 2 | 3 | It is time to test your Clojure knowledge and aptitude! 4Clojure is a collection of Clojure koans (short programming challenges) of varying difficulty. Easy problems will help novices learn and understand Clojure better, while the hard ones problems will trouble even experienced hackers. This application synchronizes with the http://www.4clojure.com and works in both online and offline mode. 4 | 5 | I thank the 4Clojure team for developing and maintaining this great resource, and for allowing me to base this application upon their work. 6 | 7 | The 4Clojure app is written under Clojure-Android initiative. Source code for the application: https://github.com/alexander-yakushev/foreclojure-android 8 | -------------------------------------------------------------------------------- /publish/shot_grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/publish/shot_grid.png -------------------------------------------------------------------------------- /publish/shot_grid_10inch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/publish/shot_grid_10inch.png -------------------------------------------------------------------------------- /publish/shot_grid_7inch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/publish/shot_grid_7inch.png -------------------------------------------------------------------------------- /publish/shot_grid_drawer_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/publish/shot_grid_drawer_open.png -------------------------------------------------------------------------------- /publish/shot_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/publish/shot_login.png -------------------------------------------------------------------------------- /publish/shot_problem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/publish/shot_problem.png -------------------------------------------------------------------------------- /publish/shot_problem_repl_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/publish/shot_problem_repl_open.png -------------------------------------------------------------------------------- /publish/shot_problem_solved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/publish/shot_problem_solved.png -------------------------------------------------------------------------------- /res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /res/drawable-hdpi/checkmark_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-hdpi/checkmark_large.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-hdpi/ic_checkmark.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-hdpi/ic_cross.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_directions_run_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-hdpi/ic_directions_run_white.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_exit_to_app_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-hdpi/ic_exit_to_app_black.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_format_list_bulleted_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-hdpi/ic_format_list_bulleted_white.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_menu_friendslist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-hdpi/ic_menu_friendslist.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_menu_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-hdpi/ic_menu_login.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_menu_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-hdpi/ic_menu_refresh.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_mode_edit_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-hdpi/ic_mode_edit_white.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_refresh_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-hdpi/ic_refresh_white.png -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_sync_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-hdpi/ic_sync_white.png -------------------------------------------------------------------------------- /res/drawable-mdpi/check_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-mdpi/check_icon.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_directions_run_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-mdpi/ic_directions_run_white.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_exit_to_app_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-mdpi/ic_exit_to_app_black.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_format_list_bulleted_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-mdpi/ic_format_list_bulleted_white.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_menu_friendslist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-mdpi/ic_menu_friendslist.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_menu_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-mdpi/ic_menu_login.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_menu_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-mdpi/ic_menu_refresh.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_mode_edit_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-mdpi/ic_mode_edit_white.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_refresh_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-mdpi/ic_refresh_white.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_sync_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-mdpi/ic_sync_white.png -------------------------------------------------------------------------------- /res/drawable-v21/card_ripple.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /res/drawable-xhdpi/foreclj_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xhdpi/foreclj_logo.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/ic_directions_run_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xhdpi/ic_directions_run_white.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/ic_exit_to_app_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xhdpi/ic_exit_to_app_black.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/ic_format_list_bulleted_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xhdpi/ic_format_list_bulleted_white.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/ic_mode_edit_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xhdpi/ic_mode_edit_white.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/ic_refresh_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xhdpi/ic_refresh_white.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/ic_sync_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xhdpi/ic_sync_white.png -------------------------------------------------------------------------------- /res/drawable-xxhdpi/ic_checkmark_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxhdpi/ic_checkmark_large.png -------------------------------------------------------------------------------- /res/drawable-xxhdpi/ic_directions_run_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxhdpi/ic_directions_run_white.png -------------------------------------------------------------------------------- /res/drawable-xxhdpi/ic_exit_to_app_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxhdpi/ic_exit_to_app_black.png -------------------------------------------------------------------------------- /res/drawable-xxhdpi/ic_format_list_bulleted_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxhdpi/ic_format_list_bulleted_white.png -------------------------------------------------------------------------------- /res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-xxhdpi/ic_mode_edit_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxhdpi/ic_mode_edit_white.png -------------------------------------------------------------------------------- /res/drawable-xxhdpi/ic_refresh_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxhdpi/ic_refresh_white.png -------------------------------------------------------------------------------- /res/drawable-xxhdpi/ic_sync_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxhdpi/ic_sync_white.png -------------------------------------------------------------------------------- /res/drawable-xxxhdpi/ic_directions_run_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxxhdpi/ic_directions_run_white.png -------------------------------------------------------------------------------- /res/drawable-xxxhdpi/ic_exit_to_app_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxxhdpi/ic_exit_to_app_black.png -------------------------------------------------------------------------------- /res/drawable-xxxhdpi/ic_format_list_bulleted_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxxhdpi/ic_format_list_bulleted_white.png -------------------------------------------------------------------------------- /res/drawable-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-xxxhdpi/ic_mode_edit_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxxhdpi/ic_mode_edit_white.png -------------------------------------------------------------------------------- /res/drawable-xxxhdpi/ic_refresh_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxxhdpi/ic_refresh_white.png -------------------------------------------------------------------------------- /res/drawable-xxxhdpi/ic_sync_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexander-yakushev/foreclojure-android/912529f9ea7c13874ffaa37a015c62a5165223b5/res/drawable-xxxhdpi/ic_sync_white.png -------------------------------------------------------------------------------- /res/drawable/card_ripple.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /res/drawable/splash_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /res/layout-land/gus.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /res/layout/gus.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 16 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /res/layout/splashscreen.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 15 | 16 | 24 | 25 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Putting that cookie down 5 | Facing Lord Clojaraxxus 6 | Dusting off stale lambdas 7 | Making simple easy 8 | Understanding transducers 9 | Writing monad tutorial 10 | Saving money for Datomic 11 | Thanking 4clojure.com devs 12 | Soldering Atreus keyboard 13 | Testing user patience 14 | 15 | 16 | Awesome 17 | Excellent 18 | Perfect 19 | Outstanding 20 | Glorious 21 | Wonderflonious 22 | Amazing 23 | Spectacular 24 | Fantastic 25 | Phenomenal 26 | Marvelous 27 | Magnificent 28 | 29 | 30 | work 31 | job 32 | execution 33 | performance 34 | achievement 35 | solution 36 | effort 37 | code 38 | 39 | 40 | -------------------------------------------------------------------------------- /res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #1A237E 6 | #C5CAE9 7 | #FFFFFF 8 | #3AAD46 9 | 10 | 11 | -------------------------------------------------------------------------------- /res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4Clojure 5 | 6 | 7 | -------------------------------------------------------------------------------- /res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /src/clojure/org/bytopia/foreclojure/api.clj: -------------------------------------------------------------------------------- 1 | (ns org.bytopia.foreclojure.api 2 | "Functions that interact with 4clojure API or fetch data directly." 3 | (:require [clojure.data.json :as json] 4 | [clojure.java.io :as io] 5 | [neko.log :as log]) 6 | (:import android.app.Activity 7 | android.util.Xml 8 | java.io.FileNotFoundException 9 | java.io.StringReader 10 | java.net.InetAddress 11 | org.apache.http.client.RedirectHandler 12 | org.apache.http.client.entity.UrlEncodedFormEntity 13 | [org.apache.http.client.methods HttpGet HttpPost] 14 | org.apache.http.cookie.Cookie 15 | org.apache.http.impl.client.DefaultHttpClient 16 | org.apache.http.message.BasicNameValuePair 17 | org.apache.http.params.CoreProtocolPNames 18 | [org.xmlpull.v1 XmlPullParser XmlPullParserFactory])) 19 | 20 | ;;; Poor man's HTTP client 21 | ;; We roll out our own basic HTTP client wrapper because Apache HTTP lib on 22 | ;; Android differs. This also frees us of few extra dependencies. The client is 23 | ;; absolutely incomplete as we implement only things we need to communicate with 24 | ;; 4clojure. 25 | 26 | (defn network-connected? [] 27 | (try 28 | (not= (InetAddress/getByName "4clojure.com") "") 29 | (catch Exception _ false))) 30 | 31 | (def ^:private ^DefaultHttpClient get-http-client 32 | "Memoized function that returns an instance of HTTP client when called." 33 | (memoize (fn [] 34 | (let [client (DefaultHttpClient.)] 35 | ;; Don't follow redirects 36 | (.setRedirectHandler 37 | client (reify RedirectHandler 38 | (getLocationURI [this response context] nil) 39 | (isRedirectRequested [this response context] false))) 40 | client)))) 41 | 42 | (defn http-post 43 | "Sends a synchronous POST request." 44 | ([data] 45 | (http-post (DefaultHttpClient.) data)) 46 | ([^DefaultHttpClient client, {:keys [url form-params]}] 47 | (let [request (HttpPost. ^String url) 48 | ;; TODO: Support headers 49 | _ (when form-params 50 | (.setEntity request (UrlEncodedFormEntity. 51 | (for [[k v] form-params] 52 | (BasicNameValuePair. k v)) 53 | "UTF-8"))) 54 | response (.execute client request)] 55 | {:status (.getStatusLine response) 56 | :body (slurp (.getContent (.getEntity response))) 57 | :redirect (when-let [loc (.getLastHeader response "Location")] 58 | (.getValue loc))}))) 59 | 60 | (defn http-get 61 | "Sends a synchronous GET request." 62 | ([data] 63 | (http-get (DefaultHttpClient.) data)) 64 | ([^DefaultHttpClient client {:keys [url]}] 65 | (let [request (HttpGet. ^String url) 66 | response (.execute client request)] 67 | {:status (.getStatusLine response) 68 | :body (slurp (.getContent (.getEntity response)))}))) 69 | 70 | ;;; XML parser 71 | ;; Again, we use a native XmlPullParser to make things faster and save 72 | ;; dependencies. 73 | 74 | (defn- problem-url->id [url] 75 | (Integer/parseInt (second (re-matches #".*/problem/(\d+)" url)))) 76 | 77 | (defn- problem-id->url [id] 78 | (str "http://www.4clojure.com/problem/" id)) 79 | 80 | (defn- problem-id->api-url [id] 81 | (str "http://www.4clojure.com/api/problem/" id)) 82 | 83 | (defmacro ^:private tag? 84 | "Helper macro for parsing. Locals `parser` and `event-type` are presumed to be 85 | bound." 86 | [start-or-end name & attrs] 87 | `(and (= ~'event-type ~(if (= start-or-end 'start) 88 | 'XmlPullParser/START_TAG 'XmlPullParser/END_TAG)) 89 | (= (.getName ~'parser) ~name) 90 | ~@(for [[k v] (partition 2 attrs)] 91 | `(= (.getAttributeValue ~'parser nil ~k) ~v)))) 92 | 93 | (defn- ^XmlPullParser html-parser 94 | [page-str] 95 | (let [^XmlPullParserFactory factory (doto (XmlPullParserFactory/newInstance) 96 | (.setValidating false) 97 | (.setFeature Xml/FEATURE_RELAXED true))] 98 | (doto (.newPullParser factory) 99 | (.setInput (StringReader. page-str))))) 100 | 101 | (defn- parse-problems-page 102 | "Because parsing HTML page is the only way to get which problems the user have 103 | solved (and also how many problems are there). `last-known-id` is the latest 104 | problem we know about and already fetched." 105 | [page-str last-known-id] 106 | (let [parser (html-parser page-str)] 107 | (loop [solved (), new (), inside-problem-table false 108 | inside-titlelink false, problem-id nil] 109 | (let [event-type (.next parser)] 110 | (cond 111 | (or (= event-type XmlPullParser/END_DOCUMENT) 112 | (and inside-problem-table 113 | (tag? end "table"))) 114 | {:solved-ids (set solved) 115 | :new-ids (sort new)} 116 | 117 | (tag? start "table", "id" "problem-table") 118 | (recur solved new true false nil) 119 | 120 | (and inside-problem-table 121 | (tag? start "td", "class" "titlelink")) 122 | (recur solved new true true nil) 123 | 124 | (and inside-titlelink 125 | (tag? start "a")) 126 | (let [id (problem-url->id (.getAttributeValue parser nil "href"))] 127 | (recur solved (if (> id last-known-id) 128 | (conj new id) new) 129 | true false id)) 130 | 131 | (and inside-problem-table 132 | (tag? start "img")) 133 | (recur (if (= (.getAttributeValue parser nil "alt") "completed") 134 | (conj solved problem-id) solved) 135 | new true false problem-id) 136 | 137 | :else (recur solved new inside-problem-table 138 | inside-titlelink problem-id)))))) 139 | 140 | (defn- parse-problem-edit-page 141 | "Parse HTML page to get the existing solution of the problem by current user." 142 | [page-str] 143 | (let [parser (html-parser page-str)] 144 | (loop [in-text-area false] 145 | (let [event-type (.next parser)] 146 | (cond 147 | (= event-type XmlPullParser/END_DOCUMENT) nil 148 | 149 | (tag? start "textarea", "id" "code-box") (recur true) 150 | 151 | (and in-text-area 152 | (= event-type XmlPullParser/TEXT)) 153 | (.getText parser) 154 | 155 | (and in-text-area 156 | (tag? end "textarea")) 157 | (recur false) 158 | 159 | :else (recur in-text-area)))))) 160 | 161 | (defn- parse-user-profile-page 162 | "Parse HTML page to get user's profile picture." 163 | [page-str] 164 | (let [parser (html-parser page-str)] 165 | (loop [] 166 | (let [event-type (.next parser)] 167 | (cond 168 | (= event-type XmlPullParser/END_DOCUMENT) nil 169 | 170 | (tag? start "img", "class" "user-profile-img") 171 | (.getAttributeValue parser nil "src") 172 | 173 | :else (recur)))))) 174 | 175 | ;;; API interaction 176 | 177 | (defn logged-in? 178 | "Checks if current HTTP client is logged in." 179 | [] 180 | (not (empty? (.getCookies (.getCookieStore (get-http-client)))))) 181 | 182 | ;; (logged-in?) 183 | 184 | (defn login 185 | "Signs into 4clojure. Returns true if the login is successful." 186 | [username password force-relogin?] 187 | (if (and (logged-in?) (not force-relogin?)) 188 | true 189 | (try 190 | (let [resp (http-post (get-http-client) 191 | {:url "http://www.4clojure.com/login" 192 | :form-params {"user" username, "pwd" password}}) 193 | success? (= (:redirect resp) "/problems")] 194 | (when-not success? 195 | ;; Don't keep the faulty cookie. 196 | (.clear (.getCookieStore (get-http-client)))) 197 | success?) 198 | (catch Exception ex 199 | (log/e "Login failed" ex) 200 | false)))) 201 | 202 | ;; (login "foo" "bar") 203 | 204 | (defn check-signup-creds-locally 205 | "Checks registration credentials before sending them to 4clojure. Returns a 206 | string if something is wrong, or nil if everything is fine. Mostly taken from 207 | 4clojure.com code." 208 | [username email pwd pwdx2] 209 | (let [username (.toLowerCase ^String username)] 210 | (cond (< (count username) 4) "Username is too short." 211 | (> (count username) 13) "Username is too long." 212 | 213 | (not= username (first (re-seq #"[A-Za-z0-9_]+" username))) 214 | "Username should contain only alphanumerics and underscore." 215 | 216 | (< (count pwd) 6) "Password is too short." 217 | (not= pwd pwdx2) "Passwords mismatch." 218 | 219 | (not (re-find #"^.+@\S+\.\S{2,4}$" email)) 220 | "Doesn't look like email, does it?"))) 221 | 222 | ;; (check-signup-creds-locally "johndoe" "john@does.com" "secret" "secret") 223 | 224 | (defn register 225 | "Registers a new account on 4clojure. Returns nil if the registration is was 226 | successful, or a error message string." 227 | [username email pwd pwdx2] 228 | (try 229 | (if-let [local-error (check-signup-creds-locally username email pwd pwdx2)] 230 | local-error 231 | (let [resp (http-post (get-http-client) 232 | {:url "http://www.4clojure.com/register" 233 | :form-params {"user" username, "email" email 234 | "pwd" pwd, "repeat-pwd" pwdx2}}) 235 | success? (= (:redirect resp) "/")] 236 | (when-not success? 237 | ;; Don't keep the faulty cookie. 238 | (.clear (.getCookieStore (get-http-client))) 239 | ;; Return error-message 240 | "Registration failed, maybe such username or email already exists?"))) 241 | (catch Exception ex 242 | (log/e "Registration failed: " username email pwd pwdx2 :exception ex) 243 | false))) 244 | 245 | (defn fetch-problem 246 | "Given a problem ID requests it from 4clojure.com using REST API. Returns 247 | parsed JSON map, or nil if problem is not found." 248 | [id] 249 | (try 250 | (-> (problem-id->api-url id) 251 | slurp 252 | json/read-str) 253 | (catch FileNotFoundException e nil))) 254 | 255 | ;; (fetch-problem 23) 256 | 257 | (defn fetch-user-solution 258 | "Given a problem ID fetches its solution submitted by the current user. If 259 | user haven't solved the problem returns empty string." 260 | [id] 261 | (let [resp (http-get (get-http-client) 262 | {:url (problem-id->url id)})] 263 | (parse-problem-edit-page (:body resp)))) 264 | 265 | ;; (fetch-user-solution 11) 266 | 267 | (defn fetch-solved-problem-ids 268 | "Given ID of the last problem we know, returns a map, where `:solved-ids` is a 269 | set of problem IDs solved by the current user; and `:new-ids` is a list of new 270 | problem IDs yet unfetched by us." 271 | [last-known-id] 272 | (let [resp (http-get (get-http-client) 273 | {:url "http://www.4clojure.com/problems"})] 274 | (parse-problems-page (:body resp) last-known-id))) 275 | 276 | ;; (fetch-solved-problem-ids 150) 277 | 278 | (defn submit-solution 279 | "Posts a solution to a problem with given ID. Presumes user to be logged in. 280 | 281 | Returns true if the solution is correct, although the opposite should never 282 | happen since we check solutions locally prior to sending them." 283 | [problem-id code] 284 | (-> (http-post (get-http-client) 285 | {:url (str "http://www.4clojure.com/rest/problem/" problem-id) 286 | :form-params {"id" (str problem-id), "code" (str code)}}) 287 | :body json/read-str 288 | (get "error") empty?)) 289 | 290 | ;; (submit-solution 11 '(/ 1 0)) 291 | ;; (submit-solution 11 '[:b 2]) 292 | 293 | (defn get-user-pic-url 294 | "Retrieves the link to user's Gravatar image." 295 | [username] 296 | (try 297 | (->> (str "http://www.4clojure.com/user/" username) 298 | slurp 299 | parse-user-profile-page 300 | (re-matches #"(^http://www.gravatar.com/avatar/[0-9a-fA-F]+)?.+") 301 | second) 302 | (catch FileNotFoundException ex nil))) 303 | 304 | ;; (get-user-pic-url "@debug") 305 | 306 | (defn download-user-pic 307 | "Downloads the image under the given URL with the specified image width. Saves 308 | the image into the internal directory with filename matching the username." 309 | [a username] 310 | (log/i "Downloading userpic for user" username) 311 | (when-let [url (get-user-pic-url username)] 312 | (with-open [in (io/input-stream (str url "?s=256&d=mm")) 313 | out (.openFileOutput a (str username ".jpg") android.content.Context/MODE_PRIVATE)] 314 | (io/copy in out) 315 | :done))) 316 | 317 | ;; (download-user-pic (neko.debug/*a :main) "@debug") 318 | 319 | -------------------------------------------------------------------------------- /src/clojure/org/bytopia/foreclojure/db.clj: -------------------------------------------------------------------------------- 1 | (ns org.bytopia.foreclojure.db 2 | (:require [clojure.data.json :as json] 3 | [clojure.java.io :as jio] 4 | [clojure.string :as str] 5 | [neko.data.sqlite :as db] 6 | [neko.debug :refer [*a]] 7 | [neko.log :as log]) 8 | (:import android.content.Context)) 9 | 10 | (def ^:private db-schema 11 | (let [nntext "text not null"] 12 | (db/make-schema 13 | :name "4clojure.db" 14 | :version 1 15 | :tables {:problems {:columns 16 | {:_id "integer primary key" 17 | :title nntext 18 | :difficulty nntext 19 | :description nntext 20 | :restricted nntext 21 | :tests nntext}} 22 | :users {:columns 23 | {:_id "integer primary key" 24 | :username nntext 25 | :password "blob"}} 26 | :solutions {:columns 27 | {:_id "integer primary key" 28 | :user_id "integer" 29 | :problem_id "integer" 30 | :is_solved "boolean" 31 | :is_synced "boolean" 32 | :code "text"}}}))) 33 | 34 | (def ^:private get-db-helper 35 | "Singleton of the SQLite helper." 36 | (memoize (fn [] (db/create-helper db-schema)))) 37 | 38 | (defn get-db 39 | "Returns a new writeable database each time it is called." 40 | [] 41 | (db/get-database (get-db-helper) :write)) 42 | 43 | ;; (def db (get-db)) 44 | 45 | (defn db-empty? 46 | "Returns true if database hasn't been yet populated with any problems." 47 | [db] 48 | (zero? (db/query-scalar db ["count" :_id] :problems nil))) 49 | 50 | ;; (db-empty? db) 51 | 52 | (defn problem-json->db 53 | "Transforms a problem map in JSON format into the one used by our SQLite 54 | database (and therefore whole application)." 55 | [json] 56 | {:pre [(map? json)]} 57 | {:_id (json "id") 58 | :title (json "title") 59 | :difficulty (json "difficulty") 60 | :description (json "description") 61 | ;; Restricted is a vector of strings. Let's serialize it in such way that we 62 | ;; later get a set of symbols in one swing. 63 | :restricted (->> (json "restricted") 64 | (interpose " ") 65 | str/join 66 | (format "#{%s}")) 67 | ;; Tests is a vector of strings. Let's keep it this way. 68 | :tests (pr-str (json "tests"))}) 69 | 70 | ;; (problem-json->db {"id" 13 "title" "Foo" "tests" ["(= __ true)" "(= __ false)"]}) 71 | 72 | (defn insert-problem 73 | ([json-map] 74 | (insert-problem (get-db) json-map)) 75 | ([db json-map] 76 | (log/d "insert-problem()" "problem-id" (get (problem-json->db json-map) "_id")) 77 | (db/insert db :problems (problem-json->db json-map)))) 78 | 79 | (defn populate-database 80 | "Inserts problems into database from the JSON file that is stored in assets." 81 | [^Context context, db] 82 | (db/transact db 83 | (with-open [stream (jio/reader (.open (.getAssets context) "data.json"))] 84 | (doseq [problem (json/read stream)] 85 | (insert-problem db problem))))) 86 | 87 | ;; (db/insert db :users {:username "@debug"}) 88 | ;; (db/insert db :users {:username "@debug2"}) 89 | ;; (db/insert db :solutions {:user_id 1, :problem_id 1, 90 | ;; :code "true", :is_solved true}) 91 | ;; (db/insert db :solutions {:user_id 2, :problem_id 2, 92 | ;; :code "4", :is_solved true}) 93 | 94 | (defn get-last-problem-id 95 | "Returns the largest problem ID in the database." 96 | [db] 97 | (db/query-scalar db ["max" :_id] :problems nil)) 98 | 99 | ;; (get-last-problem-id db) 100 | 101 | (defn initialize 102 | "Spins up the database, populates if necessary, returns the last problem ID." 103 | [context] 104 | (let [db (get-db)] 105 | (when (db-empty? db) 106 | (populate-database context db)) 107 | (get-last-problem-id db))) 108 | 109 | ;; (time (initialize (*a :main))) 110 | 111 | (defn get-problem [i] 112 | (-> (db/query-seq (get-db) :problems {:_id i}) 113 | first 114 | (update-in [:tests] read-string) 115 | (update-in [:restricted] (comp set read-string)))) 116 | 117 | ;; (get-problem (*a :main) 1) 118 | 119 | (defn update-user [username password-bytes] 120 | (let [db (get-db) 121 | user-id (db/query-scalar db :_id :users {:username username})] 122 | (if user-id 123 | (db/update db :users {:password password-bytes} {:_id user-id}) 124 | (db/insert db :users {:username username 125 | :password password-bytes})))) 126 | 127 | (defn get-user [username] 128 | (first (db/query-seq (get-db) :users {:username username}))) 129 | 130 | ;; (get-user "@debug") 131 | 132 | (defn get-solution 133 | ([username problem-id] 134 | (get-solution (get-db) username problem-id)) 135 | ([db username problem-id] 136 | (-> (db/query-seq db [:solutions/_id :solutions/code 137 | :solutions/is_solved :solutions/is_synced] 138 | [:solutions :users] 139 | {:users/_id :solutions/user_id 140 | :solutions/problem_id problem-id 141 | :users/username username}) 142 | first))) 143 | 144 | ;; (get-solution "testclient" 12) 145 | 146 | (defn update-solution 147 | "Updates user's solution to the problem. If the new solution is 148 | correct (:is_solved), force overwrite, otherwise only write the solution if 149 | the user haven't presented the correct solution to the current problem yet." 150 | [username problem-id new-solution] 151 | (log/d "Update solution:" username problem-id new-solution) 152 | (let [db (get-db) 153 | user-id (db/query-scalar db :_id :users {:username username}) 154 | old-solution (get-solution db username problem-id)] 155 | (if old-solution 156 | (when (or (:is_solved new-solution) 157 | (not (:solutions/is_solved old-solution))) 158 | (db/update db :solutions new-solution 159 | {:_id (:solutions/_id old-solution)})) 160 | (db/insert db :solutions (assoc new-solution 161 | :user_id user-id, :problem_id problem-id))))) 162 | 163 | ;; (update-solution "@debug" 1 {:code "true", :is_solved true}) 164 | 165 | (defn get-problems-cursor 166 | "Queries database for the problems to be displayed in the grid. Returns a 167 | Cursor object." 168 | [username show-solved?] 169 | (let [db (get-db) 170 | user-id (db/query-scalar db :_id :users {:username username})] 171 | (db/query 172 | db 173 | [:problems/_id :problems/title :problems/description :solutions/is_solved] 174 | (str "problems LEFT OUTER JOIN solutions ON solutions.problem_id = problems._id " 175 | "AND solutions.user_id = " user-id) 176 | (when-not show-solved? {:solutions/is_solved [:or false nil]})))) 177 | 178 | ;; (get-problems-cursor "testclient" true) 179 | 180 | (defn get-solved-ids-for-user 181 | "Returns a set of solved IDs by the given user." 182 | [username] 183 | (let [db (get-db) 184 | user-id (db/query-scalar db :_id :users {:username username})] 185 | (->> (db/query-seq db [:problem_id] :solutions {:user_id user-id, :is_solved true}) 186 | (map :problem_id) 187 | set))) 188 | 189 | ;; (get-solved-ids-for-user "@debug") 190 | 191 | ;; (db/query-seq (get-db) :solutions {:user_id 3}) 192 | 193 | (defn get-next-unsolved-id 194 | "Given a problem ID returns ID of the next unsolved problem. Returns nil if 195 | there are no unsolved problems." 196 | [username id] 197 | (let [db (get-db) 198 | user-id (db/query-scalar db :_id :users {:username username}) 199 | unsolved-ids 200 | (db/query-seq 201 | db 202 | [:problems/_id] 203 | (str "problems LEFT OUTER JOIN solutions ON solutions.problem_id = problems._id " 204 | "AND solutions.user_id = " user-id) 205 | {:solutions/is_solved [:or false nil]}) 206 | ids (sort (map :problems/_id unsolved-ids))] 207 | (if-let [next-id (first (drop-while #(<= % id) ids))] 208 | next-id 209 | ;; Otherwise all next problems are solved, give one with the smaller ID. 210 | (first ids)))) 211 | 212 | ;; (get-next-unsolved-id "@debug" 50) 213 | 214 | (defn get-solved-count-by-difficulty 215 | "Returns a vector like `[difficulty [solved-count all-count]]` for each 216 | problem difficulty for the given user." 217 | [username] 218 | (let [db (get-db) 219 | user-id (db/query-scalar db :_id :users {:username username})] 220 | (->> (db/query-seq 221 | (get-db) 222 | [:problems/_id :problems/difficulty :solutions/is_solved] 223 | (str "problems LEFT OUTER JOIN solutions ON solutions.problem_id = problems._id " 224 | "AND solutions.user_id = " user-id) 225 | nil) 226 | (group-by :problems/difficulty) 227 | (map (fn [[dif problems]] 228 | [dif [(count (filter :solutions/is_solved problems)) 229 | (count problems)]])) 230 | vec 231 | ((fn [difs] (conj difs ["Total" (reduce (fn [[acc-solved acc-all] [_ [solved all]]] 232 | [(+ acc-solved solved) (+ acc-all all)]) 233 | [0 0] difs)])))))) 234 | 235 | ;; (get-solved-count-by-difficulty "testclient") 236 | -------------------------------------------------------------------------------- /src/clojure/org/bytopia/foreclojure/logic.clj: -------------------------------------------------------------------------------- 1 | (ns org.bytopia.foreclojure.logic 2 | (:require [clojure.string :as str] 3 | clojure.walk) 4 | (:import [java.util.concurrent FutureTask TimeUnit TimeoutException] 5 | java.io.StringWriter)) 6 | 7 | (def ^:const timeout 3000) 8 | 9 | (defn thunk-timeout 10 | "Takes a function and waits for it to finish executing for the predefined 11 | number of milliseconds. Stolen from Clojail and mutilated." 12 | [thunk] 13 | (let [task (FutureTask. thunk) 14 | thread (Thread. task)] 15 | (try 16 | (.start thread) 17 | (.get task timeout TimeUnit/MILLISECONDS) 18 | (catch TimeoutException e 19 | (.cancel task true) 20 | (.interrupt thread) 21 | (throw e)) 22 | (catch Exception e 23 | (.cancel task true) 24 | (.interrupt thread) 25 | (throw e))))) 26 | 27 | ;; Poor man's clojailing here, of course it isn't enough. 28 | ;; 29 | (def ^:private forbidden-symbols 30 | #{'eval 'resolve 'read 'read-string 'throw 'ns 'in-ns 'require 'use 'refer}) 31 | 32 | (defn read-code 33 | "Safely read Clojure code from a string. Returns a sequence of read forms. 34 | Taken from 4clojure.com source." 35 | [^String code] 36 | (binding [*read-eval* false] 37 | (with-in-str code 38 | (let [end (Object.)] 39 | (doall (take-while (complement #{end}) 40 | (repeatedly #(read *in* false end)))))))) 41 | 42 | (defn check-suggested-solution 43 | "Evaluates user code against problem tests. Returns a map of test numbers to 44 | the errors (map is empty if all tests passed correctly). `restricted` is a set 45 | of symbols that are forbidden to use." 46 | [^String code, tests restricted] 47 | (reduce-kv 48 | (fn [err-map i ^String test] 49 | (if (empty? code) 50 | (assoc err-map i "Empty input is not allowed.") 51 | (try 52 | (let [user-forms (str/join " " (map pr-str (read-code code))) 53 | [code-form] (read-code (.replace test "__" user-forms)) 54 | found-restricted (clojure.walk/postwalk 55 | (fn [f] 56 | (if (sequential? f) 57 | (some identity f) 58 | (or (forbidden-symbols f) 59 | (restricted f)))) 60 | code-form)] 61 | (if (nil? found-restricted) 62 | (let [result (thunk-timeout (fn [] (eval code-form)))] 63 | (if result 64 | err-map 65 | (assoc err-map i "Unit test failed."))) 66 | (assoc err-map i 67 | (str "Form is not allowed: " 68 | found-restricted)))) 69 | (catch Throwable e (assoc err-map i 70 | (if (instance? TimeoutException e) 71 | "Execution timed out." 72 | (.getMessage e))))))) 73 | {} (vec tests))) 74 | 75 | (defn run-code-in-repl 76 | "Runs user code and returns the result and the text printed to *out*." 77 | [^String code] 78 | (let [writer (StringWriter.)] 79 | (try 80 | (let [[code-form] (read-code code)] 81 | (let [result (thunk-timeout (fn [] (binding [*out* writer] 82 | (eval code-form))))] 83 | {:result result, :out (str writer)})) 84 | (catch Throwable e 85 | {:out (str writer (if (instance? TimeoutException e) 86 | "java.util.concurrent.TimeoutException: Execution timed out." 87 | (.getMessage e)))})))) 88 | 89 | ;; (run-code-in-repl "(do (println \"foo\") (/ 1 0))") 90 | 91 | ;; (check-suggested-solution "[1 (/ 1 0) 4]" (:problem-tests @state) #{}) 92 | ;; (check-suggested-solution "6" ["(= (- 10 (* 2 3)) __)"] #{}) 93 | ;; (check-suggested-solution 94 | ;; "(loop [] (neko.log/d \"From Inside!\") 95 | ;; (Thread/sleep 500) 96 | ;; (recur))" ["(= __ true)"] #{}) 97 | -------------------------------------------------------------------------------- /src/clojure/org/bytopia/foreclojure/main.clj: -------------------------------------------------------------------------------- 1 | (ns org.bytopia.foreclojure.main 2 | (:require clojure.set 3 | [clojure.java.io :as io] 4 | [neko.activity :refer [defactivity set-content-view! get-state]] 5 | [neko.data :refer [like-map]] 6 | [neko.debug :refer [*a]] 7 | [neko.find-view :refer [find-view find-views]] 8 | [neko.log :as log] 9 | [neko.notify :refer [toast]] 10 | [neko.intent :as intent] 11 | [neko.dialog.alert :refer [alert-dialog-builder]] 12 | neko.resource 13 | [neko.threading :refer [on-ui]] 14 | [neko.ui :as ui] 15 | [neko.ui.adapters :as adapters] 16 | [neko.ui.mapping :refer [defelement]] 17 | [neko.ui.menu :as menu] 18 | [neko.ui.traits :as traits] 19 | [org.bytopia.foreclojure 20 | [db :as db] 21 | [api :as api] 22 | [utils :as utils :refer [long-running-job on-refresh-listener-call]] 23 | [user :as user]]) 24 | (:import android.app.Activity 25 | android.graphics.Color 26 | android.graphics.drawable.Drawable 27 | android.os.Build$VERSION 28 | android.text.Html 29 | android.util.LruCache 30 | android.view.View 31 | java.util.HashMap 32 | [android.widget CursorAdapter GridView TextView ProgressBar] 33 | java.util.concurrent.LinkedBlockingDeque 34 | android.support.v4.view.ViewCompat 35 | [android.support.v4.widget DrawerLayout DrawerLayout$DrawerListener] 36 | [android.support.v7.app AppCompatActivity ActionBarDrawerToggle])) 37 | 38 | (neko.resource/import-all) 39 | 40 | (defn hide-solved-problem? [] 41 | (:hide-solved? @user/prefs)) 42 | 43 | (defn refresh-ui [^Activity a] 44 | (adapters/update-cursor (.getAdapter ^GridView (find-view a ::problems-gv))) 45 | (.syncState (find-view a :neko.ui/drawer-toggle)) 46 | (.invalidateOptionsMenu a)) 47 | 48 | ;; (on-ui (refresh-ui (*a))) 49 | 50 | (declare make-navbar-header-layout) 51 | 52 | (defn load-userpic [a username] 53 | (future 54 | (log/d "Initializing userpic for" username) 55 | (let [img (io/file (.getFilesDir a) (str username ".jpg"))] 56 | (when-not (.exists img) 57 | (api/download-user-pic a username)) 58 | (when (.exists img) 59 | (on-ui (ui/config (find-view a ::navbar-userpic) 60 | :image (Drawable/createFromPath (str img)))))))) 61 | 62 | ;; (load-userpic (*a) "unlogic") 63 | 64 | (defn reload-from-server [a start-progress] 65 | (long-running-job 66 | (when start-progress (.setRefreshing (find-view a ::refresh-lay) true)) ; progress-start 67 | (.setRefreshing (find-view a ::refresh-lay) false) ; progress-stop 68 | 69 | (if (api/network-connected?) 70 | (let [user (:user (like-map (.getIntent a)))] 71 | ;; Try relogin 72 | (when (not (api/logged-in?)) 73 | (user/login-via-saved user true)) 74 | (if (api/logged-in?) 75 | (let [last-id (db/initialize a) 76 | {:keys [solved-ids new-ids]} (api/fetch-solved-problem-ids last-id) 77 | locally-solved (db/get-solved-ids-for-user user) 78 | to-download (clojure.set/difference solved-ids locally-solved) 79 | to-upload (clojure.set/difference locally-solved solved-ids)] 80 | ;; Mark new problems as solved although we don't have the code yet. 81 | (doseq [problem-id to-download] 82 | (neko.log/d "Marking problem " problem-id " as solved.") 83 | (db/update-solution user problem-id {:is_solved true, :code nil})) 84 | ;; Upload solutions to locally solved problems which aren't synced. 85 | (doseq [problem-id to-upload 86 | :let [solution (db/get-solution user problem-id)]] 87 | (neko.log/d "Sumbitting solution " problem-id solution) 88 | (if (api/submit-solution problem-id (:solutions/code solution)) 89 | (db/update-solution user problem-id {:is_synced true 90 | :is_solved true}) 91 | (on-ui (toast (str "(???) Server rejected our solution to problem " problem-id))))) 92 | ;; Download new problems and insert them into database 93 | (doseq [problem-id new-ids] 94 | (when-let [json (api/fetch-problem problem-id)] 95 | (db/insert-problem (assoc json "id" problem-id)))) 96 | (when (pos? (+ (count new-ids) (count to-download) (count to-upload))) 97 | (on-ui (toast (format "Downloaded %d new problem(s).\nDiscovered %d server solution(s).\nUploaded %d local solution(s)." 98 | (count new-ids) (count to-download) (count to-upload))))) 99 | (let [navbar (find-view a ::navbar) 100 | old-header (find-view a ::navbar-header)] 101 | (on-ui 102 | (when old-header 103 | (.removeHeaderView navbar old-header)) 104 | (.addHeaderView 105 | navbar (ui/make-ui-element 106 | a (make-navbar-header-layout user) 107 | {:id-holder (neko.activity/get-decor-view a)}))) 108 | (load-userpic a user))) 109 | (on-ui (toast "Can't login to 4clojure.com. Working in offline mode.")))) 110 | (on-ui (toast "Network is not available."))) 111 | (on-ui (refresh-ui a)))) 112 | 113 | ;; (reload-from-server (*a) true) 114 | 115 | (defn launch-problem-activity 116 | [a user problem-id] 117 | (.startActivity a (intent/intent a '.ProblemActivity 118 | {:problem-id problem-id, :user user}))) 119 | 120 | (defn switch-user [a] 121 | (user/clear-last-user a) 122 | (.startActivity a (intent/intent a '.LoginActivity {})) 123 | (.finish a)) 124 | 125 | (defn toggle-hide-solved [a] 126 | (swap! user/prefs #(assoc % :hide-solved? (not (:hide-solved? %)))) 127 | (refresh-ui a)) 128 | 129 | (defn html->short-str [html] 130 | (utils/ellipsize (str (Html/fromHtml html)) 140)) 131 | 132 | (def ^LruCache cache 133 | (memoize (fn [] (LruCache. 50)))) 134 | 135 | (defn problem-description-str [problem] 136 | (let [cache (cache) 137 | id (or (:_id problem) (:problems/_id problem)) 138 | cached-desc (.get cache id)] 139 | (if-not cached-desc 140 | (let [desc (html->short-str (or (:description problem) 141 | (:problems/description problem)))] 142 | (.put cache id desc) 143 | desc) 144 | cached-desc))) 145 | 146 | (def problem-queue (LinkedBlockingDeque.)) 147 | 148 | (defn start-preloading-thread 149 | "Spins a thread that monitors id-queue and preloads next problem descriptions 150 | onto cache. Returns a function that kills the thread." 151 | [] 152 | (let [dead-switch (atom false)] 153 | (-> (Thread. (fn [] 154 | (log/d "Preloading thread started") 155 | (while (not @dead-switch) 156 | (let [id (.take problem-queue)] 157 | (neko.log/d "Preloading from id " id) 158 | ;; Cache next 10 elements after requested one 159 | (doall (map (fn [i] 160 | (try (problem-description-str (db/get-problem i)) 161 | (catch Exception _ nil))) 162 | (range id (+ id 10)))))) 163 | (log/d "Preloading thread stopped"))) 164 | .start) 165 | (fn [] (reset! dead-switch true)))) 166 | 167 | ;; (start-preloading-thread) 168 | 169 | (defn make-problem-adapter [a user] 170 | (adapters/cursor-adapter 171 | a 172 | (fn [] 173 | [:relative-layout {:layout-height [160 :dp] 174 | :id-holder true 175 | :background-color Color/WHITE 176 | :elevation [2 :dp] 177 | :padding [5 :dp]} 178 | [:text-view {:id ::title-tv 179 | :text-size [20 :sp] 180 | :text-color (Color/rgb 33 33 33) 181 | :typeface (android.graphics.Typeface/create "sans-serif-medium" 0)}] 182 | [:text-view {:id ::desc-tv 183 | :layout-below ::title-tv 184 | :text-color (Color/rgb 33 33 33)}] 185 | [:image-view {:id ::done-iv 186 | :image R$drawable/ic_checkmark_large 187 | :layout-width [50 :dp] 188 | :layout-height [50 :dp] 189 | :layout-align-parent-bottom true 190 | :layout-align-parent-right true 191 | :layout-margin-bottom [5 :dp] 192 | :layout-margin-right [5 :dp]}]]) 193 | (fn [view _ data] 194 | (let [[^TextView title, ^TextView desc, ^View done] 195 | (find-views view ::title-tv ::desc-tv ::done-iv) 196 | 197 | id (:problems/_id data)] 198 | (.setText title ^String (str id ". " (:problems/title data))) 199 | (.setText desc ^String (problem-description-str data)) 200 | (when (= (mod id 10) 1) 201 | (.put problem-queue id)) 202 | (.setVisibility done (if (:solutions/is_solved data) 203 | View/VISIBLE View/GONE)))) 204 | (fn [] (db/get-problems-cursor user (not (hide-solved-problem?)))))) 205 | 206 | (defn make-navbar-header-layout [username] 207 | (concat 208 | [:relative-layout {:id ::navbar-header 209 | :layout-width :fill 210 | :layout-height [215 :dp]} 211 | [:image-view {:id ::navbar-userpic 212 | :layout-width [76 :dp] 213 | :layout-height [76 :dp] 214 | :layout-margin-top [10 :dp] 215 | :layout-center-horizontal true}] 216 | [:text-view {:id ::navbar-username 217 | :text username 218 | :layout-width :wrap 219 | :layout-margin-top [5 :dp] 220 | :layout-margin-bottom [10 :dp] 221 | :layout-center-horizontal true 222 | :layout-below ::navbar-userpic}]] 223 | 224 | (let [dif-pairs (->> (db/get-solved-count-by-difficulty username) 225 | (cons nil) 226 | (partition 2 1)) 227 | pb-style android.R$attr/progressBarStyleHorizontal] 228 | (->> (map (fn [[[prev-dif _] [dif [solved all]]]] 229 | (let [ns "org.bytopia.foreclojure.main" 230 | prev-tv-id (if prev-dif 231 | (keyword ns (str "navbar-diftv-" prev-dif)) 232 | ::navbar-username) 233 | curr-tv-id (keyword ns (str "navbar-diftv-" dif))] 234 | [[:text-view {:id curr-tv-id 235 | :layout-below prev-tv-id 236 | :layout-margin-left [10 :dp] 237 | :text dif}] 238 | [:progress-bar {:indeterminate false 239 | :custom-constructor 240 | (fn [c] (ProgressBar. c nil pb-style)) 241 | :layout-width [100 :dp] 242 | :layout-margin-right [10 :dp] 243 | :layout-margin-top [3 :dp] 244 | :layout-below prev-tv-id 245 | :layout-align-parent-right true 246 | :progress (int (* (/ solved (Math/max all 1)) 100))}]])) 247 | dif-pairs) 248 | (apply concat))))) 249 | 250 | (defactivity org.bytopia.foreclojure.ProblemGridActivity 251 | :key :main 252 | :extends AppCompatActivity 253 | 254 | (onCreate [this bundle] 255 | (.superOnCreate this bundle) 256 | (neko.debug/keep-screen-on this) 257 | (when (>= Build$VERSION/SDK_INT 21) 258 | (.addFlags (.getWindow this) 259 | android.view.WindowManager$LayoutParams/FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)) 260 | (on-ui 261 | (let [;; this (*a) 262 | user (:last-user @user/prefs)] 263 | (.setDisplayHomeAsUpEnabled (.getSupportActionBar this) true) 264 | (.setHomeButtonEnabled (.getSupportActionBar this) true) 265 | (intent/put-extras (.getIntent this) {:user user}) 266 | (set-content-view! this 267 | [:drawer-layout {:id ::drawer 268 | :drawer-indicator-enabled true} 269 | [:swipe-refresh-layout {:id ::refresh-lay 270 | :color-scheme-resources (into-array Integer/TYPE 271 | [R$color/blue 272 | R$color/dim_blue]) 273 | :on-refresh (fn [_] (reload-from-server this false))} 274 | [:grid-view {:id ::problems-gv 275 | :column-width (traits/to-dimension this [160 :dp]) 276 | :num-columns :auto-fit 277 | :stretch-mode :stretch-column-width 278 | :background-color (Color/rgb 229 229 229) 279 | :horizontal-spacing (traits/to-dimension this [8 :dp]) 280 | :vertical-spacing (traits/to-dimension this [8 :dp]) 281 | :padding [8 :dp] 282 | :clip-to-padding false 283 | :selector R$drawable/card_ripple 284 | :draw-selector-on-top true 285 | :adapter (make-problem-adapter this user) 286 | :on-item-click (fn [^GridView parent, _ position __] 287 | (let [id (-> (.getAdapter parent) 288 | (.getItem position) 289 | :problems/_id)] 290 | (launch-problem-activity this user id)))}]] 291 | [:navigation-view {:id ::navbar 292 | :layout-width [200 :dp] 293 | :layout-height :fill 294 | :layout-gravity :left 295 | :header (make-navbar-header-layout user) 296 | :menu [[:item {:title "Log out" 297 | :icon R$drawable/ic_exit_to_app_black 298 | :show-as-action [:always :with-text] 299 | :on-click (fn [_] (.showDialog this 0))}]]}]]) 300 | (refresh-ui this) 301 | (future (require 'org.bytopia.foreclojure.problem)) 302 | (reload-from-server this true) 303 | (load-userpic this user))) 304 | ) 305 | 306 | (onStart [this] 307 | (.superOnStart this) 308 | (refresh-ui this) 309 | (swap! (get-state this) assoc 310 | :pr-thread (start-preloading-thread))) 311 | 312 | (onStop [this] 313 | (.superOnStop this) 314 | ((:pr-thread @(get-state this)))) 315 | 316 | (onCreateOptionsMenu [this menu] 317 | (.superOnCreateOptionsMenu this menu) 318 | (let [user (:user (like-map (.getIntent this))) 319 | online? (api/logged-in?)] 320 | (menu/make-menu 321 | menu [[:item {:title "Show solved" 322 | :show-as-action :never 323 | :checkable true 324 | :checked (not (hide-solved-problem?)) 325 | :on-click (fn [_] (toggle-hide-solved this))}] 326 | [:item {:title "Reload" 327 | :icon R$drawable/ic_refresh_white 328 | :show-as-action :always 329 | :on-click (fn [_] 330 | (reload-from-server this true))}]])) 331 | true) 332 | 333 | (onOptionsItemSelected [this item] 334 | false 335 | (if (.onOptionsItemSelected (find-view this :neko.ui/drawer-toggle) item) 336 | true 337 | (.superOnOptionsItemSelected this item))) 338 | 339 | (onPostCreate [this bundle] 340 | (.superOnPostCreate this bundle) 341 | (.syncState (find-view this :neko.ui/drawer-toggle))) 342 | 343 | (onConfigurationChanged [this new-config] 344 | (.superOnConfigurationChanged this new-config) 345 | (.onConfigurationChanged (find-view this :neko.ui/drawer-toggle) new-config)) 346 | 347 | (onCreateDialog [this id _] 348 | (when (= id 0) 349 | (-> this 350 | (alert-dialog-builder 351 | {:message (str "Do you want to log out of the current account? " 352 | "Pressing OK will return you to login form.") 353 | :cancelable true 354 | :positive-text "OK" 355 | :positive-callback (fn [_ __] (switch-user this)) 356 | :negative-text "Cancel" 357 | :negative-callback (fn [dialog _] (.cancel dialog))}) 358 | .create)))) 359 | 360 | ;; (on-ui (refresh-ui (*a))) 361 | 362 | ;; (on-ui (.showDialog (*a) 0)) 363 | -------------------------------------------------------------------------------- /src/clojure/org/bytopia/foreclojure/problem.clj: -------------------------------------------------------------------------------- 1 | (ns org.bytopia.foreclojure.problem 2 | (:require [clojure.string :as str] 3 | [neko.action-bar :as action-bar] 4 | [neko.activity :refer [defactivity set-content-view! get-state]] 5 | [neko.context :refer [get-service]] 6 | [neko.data :refer [like-map]] 7 | [neko.debug :refer [*a]] 8 | [neko.find-view :refer [find-view find-views]] 9 | [neko.intent :refer [intent]] 10 | [neko.log :as log] 11 | [neko.notify :refer [toast]] 12 | neko.resource 13 | [neko.threading :refer [on-ui]] 14 | [neko.ui :as ui] 15 | [neko.ui.menu :as menu] 16 | [neko.ui.traits :as traits] 17 | [org.bytopia.foreclojure 18 | [api :as api] 19 | [db :as db] 20 | [logic :as logic] 21 | [utils :refer [long-running-job snackbar ellipsize]]]) 22 | (:import android.content.res.Configuration 23 | android.graphics.Typeface 24 | android.graphics.Color 25 | [android.text Html InputType Spannable] 26 | android.view.View 27 | android.view.inputmethod.EditorInfo 28 | [android.widget EditText ListView TextView] 29 | [org.bytopia.foreclojure SafeLinkMethod CodeboxTextWatcher] 30 | android.support.v7.app.AppCompatActivity 31 | neko.App)) 32 | 33 | (neko.resource/import-all) 34 | 35 | (defn- get-string-array [res-id] 36 | (.getStringArray (.getResources App/instance) res-id)) 37 | 38 | (defn congratulations-message [] 39 | (format "%s %s! Proceed to the next problem." 40 | (rand-nth (get-string-array R$array/nice_adjectives)) 41 | (rand-nth (get-string-array R$array/work_synonyms)))) 42 | 43 | (defmacro lrj [& body] 44 | `(long-running-job 45 | (ui/config (find-view ~'a ::eval-progress) :visibility :visible) 46 | (ui/config (find-view ~'a ::eval-progress) :visibility :gone) 47 | ~@body)) 48 | 49 | ;;; Interaction 50 | 51 | (defn repl-mode? [a] 52 | (:repl-mode @(get-state a))) 53 | 54 | (defn toggle-repl-mode [a] 55 | (let [repl? (:repl-mode (swap! (get-state a) update :repl-mode not))] 56 | (ui/config (find-view a ::repl-out) :visibility (if repl? :visible :gone)) 57 | (.invalidateOptionsMenu a))) 58 | 59 | ;; (on-ui (toggle-repl-mode (*a))) 60 | 61 | (defn update-test-status 62 | "Takes a list of pairs, one pair per test, where first value is 63 | either :good, :bad or :none; and second value is error message." 64 | [a status-list] 65 | (let [tests-lv (find-view a ::tests-lv)] 66 | (mapv (fn [test-i [imgv-state err-msg]] 67 | (let [test-view (find-view tests-lv test-i) 68 | [imgv errv] (find-views test-view ::status-iv ::error-tv)] 69 | (ui/config imgv 70 | :visibility (if (= imgv-state :none) 71 | :invisible :visible) 72 | :image (if (= imgv-state :good) 73 | R$drawable/ic_checkmark 74 | R$drawable/ic_cross)) 75 | (ui/config errv :text (str err-msg)))) 76 | (range (count (:tests (:problem @(get-state a))))) 77 | status-list))) 78 | 79 | (defn try-solution [a code] 80 | (let [{:keys [tests restricted]} (:problem @(get-state a)) 81 | results (logic/check-suggested-solution code tests restricted)] 82 | (on-ui (->> (range (count tests)) 83 | (mapv (fn [i] (let [err-msg (get results i)] 84 | [(if err-msg :bad :good) err-msg]))) 85 | (update-test-status a))) 86 | (empty? results))) 87 | 88 | ;; (on-ui (try-solution (*a) "")) 89 | 90 | (defn save-solution 91 | [a code correct?] 92 | (let [{:keys [problem-id user]} (like-map (.getIntent a))] 93 | (db/update-solution user problem-id {:code code, :is_solved correct?}))) 94 | 95 | (defn run-solution 96 | "Runs the expression in code buffer against problem tests." 97 | [a] 98 | (let [{:keys [problem-id user]} (like-map (.getIntent a)) 99 | code (str (.getText ^EditText (find-view a ::codebox)))] 100 | (lrj 101 | (let [correct? (try-solution a code)] 102 | (save-solution a code correct?) 103 | (when correct? 104 | (when (and (api/network-connected?) (api/logged-in?)) 105 | (let [success? (api/submit-solution problem-id code)] 106 | (if success? 107 | (do 108 | (db/update-solution user problem-id {:is_solved true 109 | :is_synced true}) 110 | (when-let [focused (.getCurrentFocus a)] 111 | (.hideSoftInputFromWindow (get-service :input-method) 112 | (.getWindowToken focused) 0) 113 | (Thread/sleep 100)) 114 | (let [next-id (db/get-next-unsolved-id user problem-id)] 115 | (snackbar a (congratulations-message) 116 | 20000 (when next-id "Next problem") 117 | (fn [v] 118 | (.startActivity 119 | a (intent a '.ProblemActivity 120 | {:problem-id next-id 121 | :user user})) 122 | (.finish a) 123 | (.overridePendingTransition a R$anim/slide_in_right R$anim/slide_out_left))))) 124 | (on-ui (toast "Server rejected our solution!\nPlease submit a bug report.")))))))))) 125 | 126 | ;; (run-solution (*a)) 127 | 128 | (defn run-repl 129 | "Runs the expression in code buffer as-is and show the results." 130 | [a] 131 | (let [code (str (.getText ^EditText (find-view a ::codebox))) 132 | ^TextView repl-out (find-view a ::repl-out)] 133 | (lrj 134 | (let [result (logic/run-code-in-repl code) 135 | str-to-append (str "\n" 136 | (:out result) 137 | (when (contains? result :result) 138 | (str "=> " (ellipsize (str (or (:result result) "nil")) 200))))] 139 | (on-ui (.append repl-out str-to-append)) 140 | (save-solution a code false))))) 141 | 142 | ;; (run-repl (*a)) 143 | 144 | (defn refresh-ui [a code solved?] 145 | (let [[codebox solved-iv repl-out] 146 | (find-views a ::codebox ::solved-iv ::repl-out)] 147 | (ui/config codebox :text code) 148 | (ui/config solved-iv :visibility (if solved? :visible :invisible)) 149 | (ui/config repl-out :visibility (if (repl-mode? a) :visible :gone)))) 150 | 151 | ;; (on-ui (refresh-ui (*a) "foobar" true)) 152 | 153 | (defn check-solution-on-server [a solution] 154 | (lrj 155 | (when (and (api/network-connected?) (api/logged-in?)) 156 | (let [{:keys [problem-id user]} (like-map (.getIntent a))] 157 | (when (and (:solutions/is_solved solution) 158 | (nil? (:solutions/code solution))) 159 | ;; Apparently there should be our solution on server, let's grab it. 160 | (when-let [code (api/fetch-user-solution problem-id)] 161 | (db/update-solution user problem-id {:code code, :is_solved true 162 | :is_synced true}) 163 | (on-ui (refresh-ui a code true)))))))) 164 | 165 | ;; (on-ui (check-solution-on-server (*a) nil)) 166 | 167 | (defn clear-result-flags [a] 168 | (update-test-status a (repeat [:none ""]))) 169 | 170 | (def core-forms 171 | (conj (->> (find-ns 'clojure.core) 172 | ns-map keys 173 | (keep #(re-matches #"^[a-z].*" (str %))) 174 | set) 175 | "if" "do" "recur")) 176 | 177 | ;; (on-ui (clear-result-flags (*a))) 178 | 179 | ;;; UI views 180 | 181 | (defn make-test-row [i test] 182 | [:linear-layout {:id-holder true, :id i 183 | :layout-margin-top [5 :dp] 184 | :layout-margin-bottom [5 :dp]} 185 | [:image-view {:id ::status-iv 186 | :image R$drawable/ic_checkmark 187 | :scale-type :fit-xy 188 | :layout-width [18 :dp] 189 | :layout-height [18 :dp] 190 | :visibility :invisible 191 | :layout-gravity :center}] 192 | [:text-view {:text (.replace (str test) "\\r\\n" "\n") 193 | :layout-margin-left [10 :dp] 194 | :layout-height :wrap 195 | :layout-width 0 196 | :layout-weight 1 197 | :typeface android.graphics.Typeface/MONOSPACE 198 | :layout-gravity :center}] 199 | [:text-view {:id ::error-tv 200 | :text-color (Color/rgb 200 0 0) 201 | :layout-width 0 202 | :layout-weight 1 203 | :layout-margin-left [15 :dp] 204 | :layout-gravity :center 205 | :on-click (fn [^View v] (clear-result-flags (.getContext v)))}]]) 206 | 207 | (defn make-tests-list [tests under] 208 | (list* :linear-layout {:id ::tests-lv 209 | :layout-below under 210 | :id-holder true 211 | :orientation :vertical 212 | :layout-margin-top [10 :dp] 213 | :layout-margin-left [10 :dp]} 214 | (interpose [:view {:layout-width :fill 215 | :background-color (Color/argb 31 0 0 0) 216 | :layout-height [1 :dp]}] 217 | (map-indexed make-test-row tests)))) 218 | 219 | (defn- render-html 220 | "Sometimes description comes to us in HTML. Let's make it pretty." 221 | [^String html] 222 | (let [^Spannable spannable 223 | (-> html 224 | (.replace "" "
") 225 | (.replace "
  • " "
  •   •  ") 226 | ;; Fix internal links 227 | (.replace " tag. 231 | (loop [i (dec (.length spannable))] 232 | (if (= (.charAt spannable i) \newline) 233 | (recur (dec i)) 234 | (.delete spannable (inc i) (.length spannable)))))) 235 | 236 | (defn problem-ui [a {:keys [_id title difficulty description restricted tests]}] 237 | [:scroll-view {} 238 | [:linear-layout {:id ::top-container 239 | :orientation :vertical 240 | :layout-transition (android.animation.LayoutTransition.)} 241 | [:relative-layout {:focusable true 242 | :id ::container 243 | :focusable-in-touch-mode true 244 | :background-color Color/WHITE 245 | :layout-margin [5 :dp] 246 | :layout-width :fill 247 | :elevation [1 :dp] 248 | :padding [5 :dp]} 249 | [:text-view {:id ::title-tv 250 | :text (str _id ". " title) 251 | :text-size [24 :sp] 252 | :text-color (Color/rgb 33 33 33) 253 | :typeface Typeface/DEFAULT_BOLD}] 254 | (when (= (.. a (getResources) (getConfiguration) orientation) 255 | Configuration/ORIENTATION_LANDSCAPE) 256 | [:text-view {:text difficulty 257 | :layout-align-parent-top true 258 | :layout-align-parent-right true 259 | :text-size [18 :sp] 260 | :text-color (Color/rgb 33 33 33) 261 | :padding [5 :dp]}]) 262 | [:image-view {:id ::solved-iv 263 | :layout-to-right-of ::title-tv 264 | :image R$drawable/ic_checkmark_large 265 | :layout-height [24 :sp] 266 | :layout-width [24 :sp] 267 | :layout-margin-left [10 :dp] 268 | :layout-margin-top [5 :dp]}] 269 | [:text-view {:id ::desc-tv 270 | :layout-below ::title-tv 271 | :text (render-html description) 272 | :text-color (Color/rgb 33 33 33) 273 | :movement-method (SafeLinkMethod/getInstance) 274 | :link-text-color (android.graphics.Color/rgb 0 0 139)}] 275 | (when (seq restricted) 276 | [:text-view {:id ::restricted-tv 277 | :layout-below ::desc-tv 278 | :text-color (Color/rgb 33 33 33) 279 | :text (->> restricted 280 | (str/join ", ") 281 | (str "Special restrictions: "))}]) 282 | (make-tests-list tests (if (seq restricted) 283 | ::restricted-tv ::desc-tv))] 284 | [:text-view {:id ::repl-out 285 | :layout-margin [5 :dp] 286 | :padding [3 :dp] 287 | :layout-width :fill 288 | :visibility :gone 289 | :gravity :bottom 290 | :max-lines 6 291 | :movement-method (android.text.method.ScrollingMovementMethod.) 292 | :min-height (neko.ui.traits/to-dimension a [110 :sp]) 293 | :typeface Typeface/MONOSPACE 294 | :text-color (Color/rgb 33 33 33) 295 | :background-color Color/WHITE 296 | :text ";; In REPL mode code is evaluated as-is. Press \"Run\" to see the result of your expression. *out* is redirected to here too. This view is scrollable."}] 297 | [:relative-layout {} 298 | [:progress-bar {:id ::eval-progress 299 | :layout-align-parent-right true 300 | :layout-margin-right [10 :dp] 301 | :visibility :gone}] 302 | [:edit-text {:id ::codebox 303 | :input-type (bit-or InputType/TYPE_TEXT_FLAG_NO_SUGGESTIONS 304 | InputType/TYPE_TEXT_FLAG_MULTI_LINE) 305 | :ime-options EditorInfo/IME_FLAG_NO_EXTRACT_UI 306 | :single-line false 307 | :layout-margin-top [15 :dp] 308 | :layout-width :fill 309 | :typeface Typeface/MONOSPACE 310 | :hint "Type code here"}]]]]) 311 | 312 | (defactivity org.bytopia.foreclojure.ProblemActivity 313 | :key :problem 314 | :extends AppCompatActivity 315 | 316 | (onCreate [this bundle] 317 | (.superOnCreate this bundle) 318 | (neko.debug/keep-screen-on this) 319 | (let [;; this (*a) 320 | ] 321 | (on-ui 322 | (let [{:keys [problem-id user]} (like-map (.getIntent this)) 323 | problem (db/get-problem problem-id) 324 | solution (db/get-solution user problem-id) 325 | code (or (:solutions/code solution) "") 326 | solved? (and solution (:solutions/is_solved solution))] 327 | (swap! (get-state this) assoc :problem problem, :solution solution) 328 | (set-content-view! this (problem-ui this problem)) 329 | (.addTextChangedListener ^EditText (find-view this ::codebox) 330 | (CodeboxTextWatcher. core-forms)) 331 | (refresh-ui this code solved?) 332 | (.setDisplayHomeAsUpEnabled (.getSupportActionBar this) true) 333 | (.setHomeButtonEnabled (.getSupportActionBar this) true) 334 | (.setTitle (.getSupportActionBar this) (str "Problem " (:_id problem))) 335 | (check-solution-on-server this solution)))) 336 | ) 337 | 338 | (onStop [this] 339 | (.superOnStop this) 340 | (let [code (str (.getText ^EditText (find-view this ::codebox)))] 341 | (when-not (= code "") 342 | (save-solution this code false)))) 343 | 344 | (onCreateOptionsMenu [this menu] 345 | (.superOnCreateOptionsMenu this menu) 346 | (let [repl-mode (repl-mode? this)] 347 | (menu/make-menu 348 | menu [[:item {:title (if repl-mode 349 | "Switch to problem mode" 350 | "Switch to REPL mode") 351 | :icon (if repl-mode 352 | R$drawable/ic_format_list_bulleted_white 353 | R$drawable/ic_mode_edit_white) 354 | :show-as-action :always 355 | :on-click (fn [_] (toggle-repl-mode this))}] 356 | [:item {:title "Run" 357 | :icon R$drawable/ic_directions_run_white 358 | :show-as-action [:always :with-text] 359 | :on-click (fn [_] (if (repl-mode? this) 360 | (run-repl this) 361 | (run-solution this)))}]])) 362 | true) 363 | 364 | (onOptionsItemSelected [this item] 365 | (if (= (.getItemId item) android.R$id/home) 366 | (.finish this) 367 | (.superOnOptionsItemSelected this item)) 368 | true) 369 | 370 | (onSaveInstanceState [this bundle] 371 | (.putBoolean bundle "repl-mode" (boolean (repl-mode? this))) 372 | (.putString bundle "repl-out" (str (.getText (find-view this ::repl-out))))) 373 | 374 | (onRestoreInstanceState [this bundle] 375 | (let [b (like-map bundle)] 376 | (ui/config (find-view this ::repl-out) :text (:repl-out b)) 377 | (when (:repl-mode b) 378 | (toggle-repl-mode this))))) 379 | -------------------------------------------------------------------------------- /src/clojure/org/bytopia/foreclojure/user.clj: -------------------------------------------------------------------------------- 1 | (ns org.bytopia.foreclojure.user 2 | (:require clojure.set 3 | [neko.activity :refer [defactivity set-content-view! get-state]] 4 | [neko.data.shared-prefs :as sp] 5 | [neko.debug :refer [*a]] 6 | [neko.find-view :refer [find-view find-views]] 7 | [neko.notify :refer [toast]] 8 | [neko.threading :refer [on-ui]] 9 | [neko.ui :as ui] 10 | neko.ui.adapters 11 | [neko.intent :refer [intent]] 12 | neko.resource 13 | [org.bytopia.foreclojure 14 | [db :as db] 15 | [api :as api] 16 | [utils :refer [long-running-job]]]) 17 | (:import [android.app ProgressDialog Activity] 18 | android.content.res.Configuration 19 | android.text.Html 20 | android.text.InputType 21 | android.view.View 22 | android.view.WindowManager$LayoutParams 23 | android.widget.EditText 24 | javax.crypto.Cipher 25 | javax.crypto.SecretKey 26 | javax.crypto.spec.SecretKeySpec 27 | [org.bytopia.foreclojure BuildConfig SafeLinkMethod])) 28 | 29 | (neko.resource/import-all) 30 | 31 | (def ^SecretKeySpec secret-key 32 | (SecretKeySpec. (.getBytes BuildConfig/ENC_KEY) 33 | BuildConfig/ENC_ALGORITHM)) 34 | 35 | (defn- encrypt-pwd [^String password] 36 | (let [cipher (doto (Cipher/getInstance BuildConfig/ENC_ALGORITHM) 37 | (.init Cipher/ENCRYPT_MODE secret-key))] 38 | (.doFinal cipher (.getBytes password)))) 39 | 40 | (defn- decrypt-pwd [^bytes password-bytes] 41 | (let [cipher (doto (Cipher/getInstance BuildConfig/ENC_ALGORITHM) 42 | (.init Cipher/DECRYPT_MODE secret-key))] 43 | (String. (.doFinal cipher password-bytes)))) 44 | 45 | (defn set-user-db [username password] 46 | (db/update-user username (encrypt-pwd password))) 47 | 48 | (defn lookup-user [username] 49 | (update-in (db/get-user username) [:password] decrypt-pwd)) 50 | 51 | (sp/defpreferences prefs "4clojure") 52 | 53 | (defn set-last-user [a username] 54 | (swap! prefs assoc :last-user username)) 55 | 56 | (defn clear-last-user [a] 57 | (swap! prefs dissoc :last-user)) 58 | 59 | (defn login-via-input [a] 60 | (let [[user-et pwd-et] (find-views a ::user-et ::pwd-et) 61 | username (str (.getText ^EditText user-et)) 62 | password (str (.getText ^EditText pwd-et)) 63 | progress (ProgressDialog/show a nil "Signing in..." true)] 64 | (neko.log/d "login-via-input()" "username" username "password" password) 65 | (future 66 | (try 67 | (if-let [success? (api/login username password true)] 68 | (do (set-last-user a username) 69 | (set-user-db username password) 70 | (.startActivity a (intent a '.ProblemGridActivity {})) 71 | (.finish a)) 72 | (on-ui a (toast "Could not sign in. Please check the correctness of your credentials." :short))) 73 | (finally (on-ui (.dismiss progress))))))) 74 | 75 | (defn login-via-saved [username force?] 76 | (let [pwd (:password (lookup-user username))] 77 | (api/login username pwd force?))) 78 | 79 | (defn register [^Activity a] 80 | (let [[username email pwd pwdx2 :as creds] 81 | (map (fn [^EditText et] (str (.getText ^EditText et))) 82 | (find-views a ::user-et ::email-et ::pwd-et ::pwdx2-et)) 83 | 84 | progress (ProgressDialog/show a nil "Signing up..." true)] 85 | (neko.log/d "register()" "creds:" creds) 86 | (future 87 | (try 88 | (let [error (apply api/register creds)] 89 | (if-not error 90 | (do (set-last-user a username) 91 | (set-user-db username pwd) 92 | (.startActivity a (intent a '.ProblemGridActivity {})) 93 | (.finish a)) 94 | (on-ui a (toast error)))) 95 | (catch Exception ex (on-ui (toast (str "Exception raised: " ex)))) 96 | (finally (on-ui (.dismiss progress))))))) 97 | 98 | (defn refresh-ui [a] 99 | (let [signup-active? (:signup-active? @(.state a)) 100 | [signin signup signup-layout] (find-views a ::signin-but ::signup-but 101 | ::email-and-pwdx2)] 102 | (ui/config signin :text (if signup-active? 103 | "Wait, I got it" "Sign in")) 104 | (ui/config signup :text (if signup-active? 105 | "Register" "No account?")) 106 | (ui/config signup-layout :visibility (if signup-active? 107 | :visible :gone)))) 108 | 109 | (defn login-form [where] 110 | (let [basis {:layout-width 0, :layout-weight 1} 111 | basis-edit (assoc basis :ime-options android.view.inputmethod.EditorInfo/IME_FLAG_NO_EXTRACT_UI 112 | :gravity :center-horizontal)] 113 | [:linear-layout {where ::gus-logo 114 | :orientation :vertical 115 | :layout-width :fill 116 | :layout-height :fill 117 | :gravity :center 118 | :layout-transition (android.animation.LayoutTransition.)} 119 | [:linear-layout {:layout-width :fill 120 | :layout-margin [10 :dp]} 121 | [:edit-text (assoc basis-edit 122 | :id ::user-et 123 | :input-type (bit-or InputType/TYPE_CLASS_TEXT 124 | InputType/TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) 125 | :hint "username")] 126 | [:edit-text (assoc basis-edit 127 | :id ::pwd-et 128 | :layout-margin-left [10 :dp] 129 | :input-type (bit-or InputType/TYPE_CLASS_TEXT 130 | InputType/TYPE_TEXT_VARIATION_PASSWORD) 131 | :hint "password")]] 132 | [:linear-layout {:id ::email-and-pwdx2 133 | :layout-width :fill 134 | :layout-margin [10 :dp]} 135 | [:edit-text (assoc basis-edit 136 | :id ::email-et 137 | :input-type (bit-or InputType/TYPE_CLASS_TEXT 138 | InputType/TYPE_TEXT_VARIATION_EMAIL_ADDRESS) 139 | :hint "email")] 140 | [:edit-text (assoc basis-edit 141 | :id ::pwdx2-et 142 | :layout-margin-left [10 :dp] 143 | :input-type (bit-or InputType/TYPE_CLASS_TEXT 144 | InputType/TYPE_TEXT_VARIATION_PASSWORD) 145 | :hint "password x2")]] 146 | [:linear-layout {:layout-width :fill 147 | :layout-margin-top [10 :dp] 148 | :layout-margin-left [20 :dp] 149 | :layout-margin-right [20 :dp]} 150 | [:button (assoc basis 151 | :id ::signin-but 152 | :on-click (fn [w] 153 | (let [a (.getContext w) 154 | state (get-state a)] 155 | (if (:signup-active? @state) 156 | (do (swap! state assoc :signup-active? false) 157 | (refresh-ui a)) 158 | (login-via-input a)))))] 159 | [:button (assoc basis 160 | :id ::signup-but 161 | :layout-margin-left [30 :dp] 162 | :on-click (fn [^View w] 163 | (let [a (.getContext w) 164 | state (get-state a)] 165 | (if (not (:signup-active? @state)) 166 | (do (swap! state assoc :signup-active? true) 167 | (refresh-ui a)) 168 | (register a)))))]]])) 169 | 170 | (defn activity-ui [landscape?] 171 | [:scroll-view {:layout-width :fill 172 | :layout-height :fill 173 | :fill-viewport true} 174 | [:relative-layout {:layout-width :fill 175 | :layout-height :fill} 176 | [:linear-layout (cond-> {:id ::gus-logo 177 | :orientation :vertical 178 | :gravity :center 179 | :layout-margin [10 :dp]} 180 | landscape? (assoc :layout-center-vertical true) 181 | (not landscape?) (assoc :layout-center-horizontal true)) 182 | [:text-view {:id ::welcome-tv 183 | :text "Welcome to 4Clojure*!" 184 | :text-size [22 :sp]}] 185 | [:image-view {:image R$drawable/foreclj_logo 186 | :layout-height (if landscape? [250 :dp] [320 :dp])}] 187 | [:text-view {:text (Html/fromHtml "*This is an unofficial client for 4clojure.com") 188 | :movement-method (SafeLinkMethod/getInstance) 189 | :text-size [14 :sp] 190 | :padding-right [5 :dp] 191 | :link-text-color (android.graphics.Color/rgb 0 0 139)}]] 192 | (login-form (if landscape? 193 | :layout-to-right-of 194 | :layout-below))]]) 195 | 196 | (defactivity org.bytopia.foreclojure.LoginActivity 197 | :key :user 198 | :features [:indeterminate-progress :no-title] 199 | 200 | (onCreate [this bundle] 201 | (.superOnCreate this bundle) 202 | (neko.debug/keep-screen-on this) 203 | (.. this (getWindow) (setSoftInputMode WindowManager$LayoutParams/SOFT_INPUT_STATE_HIDDEN)) 204 | (let [;; this (*a) 205 | landscape? (= (ui/get-screen-orientation) :landscape)] 206 | (on-ui 207 | (set-content-view! this (activity-ui landscape?)) 208 | (refresh-ui this))) 209 | )) 210 | -------------------------------------------------------------------------------- /src/clojure/org/bytopia/foreclojure/utils.clj: -------------------------------------------------------------------------------- 1 | (ns org.bytopia.foreclojure.utils 2 | (:require [neko.activity :as a] 3 | [neko.listeners.view :refer [on-click-call]] 4 | [neko.notify :refer [toast]] 5 | [neko.resource :refer [get-string]] 6 | [neko.threading :refer [on-ui]] 7 | [neko.ui :as ui] 8 | [neko.ui.mapping :refer [defelement]] 9 | [neko.ui.menu :as menu] 10 | [neko.ui.traits :as traits] 11 | [neko.-utils :as u]) 12 | (:import android.app.Activity 13 | android.view.View 14 | android.support.v4.widget.SwipeRefreshLayout$OnRefreshListener 15 | android.support.design.widget.Snackbar 16 | android.support.v4.view.ViewCompat 17 | [android.support.v4.widget DrawerLayout DrawerLayout$DrawerListener] 18 | [android.support.v7.app AppCompatActivity ActionBarDrawerToggle] 19 | java.util.HashMap)) 20 | 21 | (defn ellipsize [s max-length] 22 | (let [lng (count s)] 23 | (if (> lng max-length) 24 | (str (subs s 0 max-length) "…") s))) 25 | 26 | (defmacro long-running-job 27 | "Runs body in a future, initializing progress with `progress-start` 28 | expression, and ending it with `progress-stop`." 29 | [progress-start progress-stop & body] 30 | (let [asym (with-meta 'a {:tag android.app.Activity})] 31 | `(future 32 | (on-ui ~progress-start) 33 | (try ~@body 34 | (catch Exception ex# (on-ui (toast (str ex#)))) 35 | (finally (on-ui ~progress-stop)))))) 36 | 37 | (defn on-refresh-listener-call 38 | [callback swipe-layout] 39 | (reify SwipeRefreshLayout$OnRefreshListener 40 | (onRefresh [_] 41 | (u/call-if-nnil callback swipe-layout)))) 42 | 43 | (defn snackbar 44 | ([view-or-activity text duration] 45 | (snackbar view-or-activity text duration nil nil)) 46 | ([view-or-activity text duration action-text action-callback] 47 | (let [sb (Snackbar/make ^View (if (instance? Activity view-or-activity) 48 | (a/get-decor-view view-or-activity) 49 | view-or-activity) 50 | (get-string text) 51 | duration)] 52 | (when action-text 53 | (.setAction sb action-text (on-click-call action-callback))) 54 | (.show sb)))) 55 | 56 | (neko.ui.mapping/defelement :drawer-layout 57 | :classname android.support.v4.widget.DrawerLayout 58 | :inherits :view-group 59 | :traits [:drawer-toggle]) 60 | 61 | (neko.ui.mapping/defelement :navigation-view 62 | :classname android.support.design.widget.NavigationView 63 | :inherits :frame-layout 64 | :traits [:navbar-menu :navbar-header-view]) 65 | 66 | (neko.ui.traits/deftrait :drawer-layout-params 67 | "docs" 68 | {:attributes (concat (deref #'neko.ui.traits/margin-attributes) 69 | [:layout-width :layout-height 70 | :layout-weight :layout-gravity]) 71 | :applies? (= container-type :drawer-layout)} 72 | [^View wdg, {:keys [layout-width layout-height layout-weight layout-gravity] 73 | :as attributes} 74 | {:keys [container-type]}] 75 | (let [^int width (->> (or layout-width :wrap) 76 | (neko.ui.mapping/value :layout-params) 77 | (neko.ui.traits/to-dimension (.getContext wdg))) 78 | ^int height (->> (or layout-height :wrap) 79 | (neko.ui.mapping/value :layout-params) 80 | (neko.ui.traits/to-dimension (.getContext wdg))) 81 | weight (or layout-weight 0) 82 | params (android.support.v4.widget.DrawerLayout$LayoutParams. width height weight)] 83 | (#'neko.ui.traits/apply-margins-to-layout-params (.getContext wdg) params attributes) 84 | (when layout-gravity 85 | (set! (. params gravity) 86 | (neko.ui.mapping/value :layout-params layout-gravity :gravity))) 87 | (.setLayoutParams wdg params))) 88 | 89 | (neko.ui.traits/deftrait :drawer-toggle 90 | "docs" 91 | {:attributes [:drawer-open-text :drawer-closed-text :drawer-indicator-enabled 92 | :on-drawer-closed :on-drawer-opened]} 93 | [^DrawerLayout wdg, {:keys [drawer-open-text drawer-closed-text 94 | drawer-indicator-enabled 95 | on-drawer-opened on-drawer-closed]} 96 | {:keys [^View id-holder]}] 97 | (let [toggle (proxy [ActionBarDrawerToggle DrawerLayout$DrawerListener] 98 | [^android.app.Activity (.getContext wdg) 99 | wdg 100 | ^int (or drawer-open-text android.R$string/untitled) 101 | ^int (or drawer-closed-text android.R$string/untitled)] 102 | (onDrawerOpened [view] 103 | (neko.-utils/call-if-nnil on-drawer-opened view)) 104 | (onDrawerClosed [view] 105 | (neko.-utils/call-if-nnil on-drawer-closed view)))] 106 | (.setDrawerIndicatorEnabled toggle (boolean drawer-indicator-enabled)) 107 | (.setDrawerListener wdg toggle) 108 | (when id-holder 109 | (.put ^HashMap (.getTag id-holder) :neko.ui/drawer-toggle toggle)))) 110 | 111 | (neko.ui.mapping/defelement :swipe-refresh-layout 112 | :classname android.support.v4.widget.SwipeRefreshLayout 113 | :traits [:on-refresh]) 114 | 115 | (neko.ui.traits/deftrait :on-refresh 116 | "docs " 117 | [^android.support.v4.widget.SwipeRefreshLayout wdg, {:keys [on-refresh]} _] 118 | (.setOnRefreshListener wdg (on-refresh-listener-call on-refresh wdg))) 119 | 120 | (neko.ui.traits/deftrait :elevation 121 | "docs " 122 | [^View wdg, {:keys [elevation]} _] 123 | (ViewCompat/setElevation wdg (neko.ui.traits/to-dimension 124 | (.getContext wdg) elevation))) 125 | 126 | (neko.ui.traits/deftrait :navbar-header-view 127 | "docs " 128 | {:attributes [:header]} 129 | [^android.support.design.widget.NavigationView wdg, {:keys [header]} opts] 130 | (.addHeaderView wdg (ui/make-ui-element (.getContext wdg) header opts))) 131 | 132 | (neko.ui.traits/deftrait :navbar-menu 133 | "docs " 134 | {:attributes [:menu]} 135 | [^android.support.design.widget.NavigationView wdg, {:keys [menu]} _] 136 | (menu/make-menu (.getMenu wdg) menu)) 137 | 138 | (neko.ui.mapping/add-trait! :view :drawer-layout-params) 139 | (neko.ui.mapping/add-trait! :view :elevation) 140 | (neko.ui.mapping/add-trait! :swipe-refresh-layout :drawer-layout-params) 141 | 142 | (swap! (deref #'neko.ui.mapping/reverse-mapping) assoc android.widget.ProgressBar :progress-bar) 143 | -------------------------------------------------------------------------------- /src/java/org/bytopia/foreclojure/CodeboxTextWatcher.java: -------------------------------------------------------------------------------- 1 | package org.bytopia.foreclojure; 2 | 3 | import android.graphics.Color; 4 | import android.text.Editable; 5 | import android.text.Spanned; 6 | import android.text.TextWatcher; 7 | import android.text.style.ForegroundColorSpan; 8 | import clojure.lang.PersistentHashSet; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | import android.util.Log; 12 | 13 | public class CodeboxTextWatcher implements TextWatcher { 14 | 15 | private static String TAG = "org.bytopia.foreclojure.CodeboxTextWatcher"; 16 | private PersistentHashSet coreForms; 17 | private final static PersistentHashSet defunIndents = 18 | PersistentHashSet.create("fn", "let", "if", "when", "if-let", "when-let", 19 | "if-not", "when-not", "loop", "for", "doseq", "while"); 20 | private int editPoint = 0; 21 | private boolean enterPressed = false; 22 | private Pattern formPattern = Pattern.compile("[-?a-z]+"); 23 | 24 | public CodeboxTextWatcher(PersistentHashSet coreForms) { 25 | this.coreForms = coreForms; 26 | } 27 | 28 | private static int calculateIndent(String txt) { 29 | String[] lines = txt.split("\n"); 30 | int closingP = 0; 31 | int closingB = 0; 32 | int closingC = 0; 33 | for (int i = lines.length - 1; i >= 0; i--) { 34 | String cl = lines[i]; 35 | for (int j = cl.length() - 1; j >= 0; j--) { 36 | char c = cl.charAt(j); 37 | switch (c) { 38 | case ')': closingP++; break; 39 | case ']': closingB++; break; 40 | case '}': closingC++; break; 41 | case '(': closingP--; break; 42 | case '[': closingB--; break; 43 | case '{': closingC--; break; 44 | } 45 | Log.d(TAG, "calculate: " + c + " " + closingP + closingB + closingC); 46 | if (closingB < 0 || closingC < 0) { 47 | return j + 1; 48 | } else if (closingP < 0) { 49 | String[] words = cl.substring(j+1).split("[ ,]"); 50 | if (words.length <= 1) 51 | return j + 1; 52 | else if (defunIndents.contains(words[0])) 53 | return j + 2; 54 | else 55 | return j + words[0].length() + 2; 56 | } 57 | } 58 | } 59 | return 0; 60 | } 61 | 62 | private static String makeSpaces(int count) { 63 | if (count == 0) return ""; 64 | StringBuilder b = new StringBuilder(); 65 | for (int i = 0; i < count; i++) 66 | b.append(" "); 67 | return b.toString(); 68 | } 69 | 70 | @Override 71 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 72 | 73 | @Override 74 | public void onTextChanged(CharSequence s, int start, int before, int count) { 75 | try { 76 | editPoint = start; 77 | enterPressed = ((s.length() > 0) && (count > 0) && 78 | (s.charAt(start) == '\n')); 79 | } catch (Exception e) { Log.e(TAG, "onTextChanged: " + s + " " + start); } 80 | } 81 | 82 | @Override 83 | public void afterTextChanged(Editable s) { 84 | try { 85 | // Handle Enter pressed 86 | if (enterPressed) { 87 | int spacesCount = calculateIndent(s.toString().substring(0, editPoint)); 88 | s.insert(editPoint+1, makeSpaces(spacesCount)); 89 | } 90 | 91 | // Remove spans at the point of editing 92 | for (ForegroundColorSpan span : s.getSpans(editPoint, editPoint, 93 | ForegroundColorSpan.class)) { 94 | s.removeSpan(span); 95 | } 96 | 97 | // Add new spans for Clojure forms 98 | Matcher m = formPattern.matcher(s.toString()); 99 | while (m.find()) { 100 | String form = (String)coreForms.get(m.group()); 101 | if (form != null) { 102 | ForegroundColorSpan[] spans = s.getSpans(m.start(), m.end(), 103 | ForegroundColorSpan.class); 104 | if (spans.length == 0) { 105 | s.setSpan(new ForegroundColorSpan(Color.BLUE), 106 | m.start(), m.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 107 | } 108 | } 109 | } 110 | } catch (Exception e) { Log.e(TAG, "afterTextChanged: " + s + " " + editPoint); } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/java/org/bytopia/foreclojure/SafeLinkMethod.java: -------------------------------------------------------------------------------- 1 | package org.bytopia.foreclojure; 2 | 3 | import android.widget.TextView; 4 | import android.text.Spannable; 5 | import android.text.method.LinkMovementMethod; 6 | import android.view.MotionEvent; 7 | 8 | public class SafeLinkMethod extends LinkMovementMethod { 9 | 10 | private static SafeLinkMethod instance; 11 | 12 | private SafeLinkMethod() { } 13 | 14 | public static SafeLinkMethod getInstance() { 15 | if (instance == null) { 16 | instance = new SafeLinkMethod(); 17 | } 18 | return instance; 19 | } 20 | 21 | @Override 22 | public boolean onTouchEvent(TextView widget, Spannable buffer, 23 | MotionEvent event) { 24 | try { 25 | return super.onTouchEvent( widget, buffer, event ) ; 26 | } catch( Exception ex ) { 27 | return true; 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/java/org/bytopia/foreclojure/SplashActivity.java: -------------------------------------------------------------------------------- 1 | package org.bytopia.foreclojure; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.SharedPreferences; 7 | import android.graphics.drawable.TransitionDrawable; 8 | import android.os.Bundle; 9 | import android.view.animation.Animation; 10 | import android.view.animation.AnimationUtils; 11 | import android.widget.ImageView; 12 | import android.widget.TextView; 13 | 14 | import neko.App; 15 | 16 | import android.graphics.Canvas; 17 | import android.graphics.Color; 18 | import android.graphics.drawable.Drawable; 19 | import android.graphics.drawable.ColorDrawable; 20 | import android.graphics.drawable.LayerDrawable; 21 | import android.os.SystemClock; 22 | 23 | import org.bytopia.foreclojure.R; 24 | 25 | public class SplashActivity extends Activity { 26 | 27 | private static boolean firstLaunch = true; 28 | private static boolean inProgress = false; 29 | private static String TAG = "Splash"; 30 | 31 | @Override 32 | public void onCreate(Bundle bundle) { 33 | super.onCreate(bundle); 34 | 35 | if (firstLaunch) { 36 | setupSplash(); 37 | if (!inProgress) 38 | App.loadAsynchronously("org.bytopia.foreclojure.LoginActivity", 39 | new Runnable() { 40 | @Override 41 | public void run() { 42 | proceed(); 43 | }}); 44 | inProgress = true; 45 | } else { 46 | proceed(); 47 | } 48 | } 49 | 50 | public void setupSplash() { 51 | setContentView(R.layout.splashscreen); 52 | 53 | TextView loading = (TextView)findViewById(R.id.splash_loading_message); 54 | String[] messages = getResources().getStringArray(R.array.loading_messages); 55 | int idx = (int)Math.floor(Math.random() * messages.length); 56 | String msg = messages[idx]; 57 | if (msg == null) 58 | msg = "Catching exceptions"; 59 | loading.setText(msg + ", please wait..."); 60 | 61 | ImageView eye = (ImageView)findViewById(R.id.splash_gus_eye); 62 | CyclicTransitionDrawable ctd = new CyclicTransitionDrawable(new Drawable[] { 63 | new ColorDrawable(Color.RED), new ColorDrawable(Color.GREEN) 64 | }); 65 | eye.setImageDrawable(ctd); 66 | ctd.startTransition(750, 0); 67 | } 68 | 69 | public void proceed() { 70 | SharedPreferences prefs = getSharedPreferences("4clojure", Context.MODE_PRIVATE); 71 | String lastUser = prefs.getString("last-user", null); 72 | Class activity; 73 | try { 74 | if (lastUser != null) { 75 | activity = Class.forName("org.bytopia.foreclojure.ProblemGridActivity"); 76 | } else { 77 | activity = Class.forName("org.bytopia.foreclojure.LoginActivity"); 78 | } 79 | startActivity(new Intent(this, activity)); 80 | inProgress = false; 81 | firstLaunch = false; 82 | finish(); 83 | } catch (Exception ex) { throw (RuntimeException)ex; } 84 | } 85 | 86 | // Code by Chris Blunt from StackOverflow 87 | private static class CyclicTransitionDrawable extends LayerDrawable implements Drawable.Callback { 88 | 89 | protected enum TransitionState { 90 | STARTING, PAUSED, RUNNING 91 | } 92 | 93 | Drawable[] drawables; 94 | int currentDrawableIndex; 95 | int alpha = 0; 96 | int fromAlpha; 97 | int toAlpha; 98 | long duration; 99 | long startTimeMillis; 100 | long pauseDuration; 101 | TransitionState transitionStatus; 102 | 103 | public CyclicTransitionDrawable(Drawable[] drawables) { 104 | super(drawables); 105 | this.drawables = drawables; 106 | } 107 | public void startTransition(int durationMillis, int pauseTimeMillis) { 108 | fromAlpha = 0; 109 | toAlpha = 255; 110 | duration = durationMillis; 111 | pauseDuration = pauseTimeMillis; 112 | startTimeMillis = SystemClock.uptimeMillis(); 113 | transitionStatus = TransitionState.PAUSED; 114 | currentDrawableIndex = 0; 115 | invalidateSelf(); 116 | } 117 | @Override 118 | public void draw(Canvas canvas) { 119 | boolean done = true; 120 | switch (transitionStatus) { 121 | case STARTING: 122 | done = false; 123 | transitionStatus = TransitionState.RUNNING; 124 | break; 125 | case PAUSED: 126 | if ((SystemClock.uptimeMillis() - startTimeMillis) < pauseDuration) 127 | break; 128 | else { 129 | done = false; 130 | startTimeMillis = SystemClock.uptimeMillis(); 131 | transitionStatus = TransitionState.RUNNING; 132 | } 133 | case RUNNING: 134 | break; 135 | } 136 | // Determine position within the transition cycle 137 | if (startTimeMillis >= 0) { 138 | float normalized = (float) (SystemClock.uptimeMillis() - startTimeMillis) / duration; 139 | done = normalized >= 1.0f; 140 | normalized = Math.min(normalized, 1.0f); 141 | alpha = (int) (fromAlpha + (toAlpha - fromAlpha) * normalized); 142 | } 143 | if (transitionStatus == TransitionState.RUNNING) { 144 | // Cross fade the current 145 | int nextDrawableIndex = 0; 146 | if (currentDrawableIndex + 1 < drawables.length) 147 | nextDrawableIndex = currentDrawableIndex + 1; 148 | Drawable currentDrawable = getDrawable(currentDrawableIndex); 149 | Drawable nextDrawable = getDrawable(nextDrawableIndex); 150 | // Apply cross fade and draw the current drawable 151 | currentDrawable.setAlpha(255 - alpha); 152 | currentDrawable.draw(canvas); 153 | currentDrawable.setAlpha(0xFF); 154 | if (alpha > 0) { 155 | nextDrawable.setAlpha(alpha); 156 | nextDrawable.draw(canvas); 157 | nextDrawable.setAlpha(0xFF); 158 | } 159 | // If we have finished, move to the next transition 160 | if (done) { 161 | currentDrawableIndex = nextDrawableIndex; 162 | startTimeMillis = SystemClock.uptimeMillis(); 163 | transitionStatus = TransitionState.PAUSED; 164 | } 165 | } 166 | else 167 | getDrawable(currentDrawableIndex).draw(canvas); 168 | invalidateSelf(); 169 | } 170 | } 171 | } 172 | --------------------------------------------------------------------------------