├── .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 |
--------------------------------------------------------------------------------