31 | * @since 0.7
32 | */
33 | public static class Builder {
34 |
35 | private String encoding = "UTF-8";
36 | private Decorator decorator;
37 |
38 | /**
39 | * Constructor.
40 | */
41 | Builder() {
42 | // empty
43 | }
44 |
45 | /**
46 | * Sets the character encoding for txtmark.
47 | *
48 | * Default: "UTF-8"
49 | *
50 | * @param encoding
51 | * The encoding
52 | * @return This builder
53 | * @since 0.7
54 | */
55 | public Builder setEncoding(String encoding) {
56 | this.encoding = encoding;
57 | return this;
58 | }
59 |
60 | /**
61 | * Builds a configuration instance.
62 | *
63 | * @return a Configuration instance
64 | * @since 0.7
65 | */
66 | public Configuration build() {
67 | return new Configuration(this.encoding, this.decorator);
68 | }
69 |
70 | public Decorator getDecorator() {
71 | return decorator;
72 | }
73 |
74 | /**
75 | * Sets the decorator for txtmark.
76 | *
77 | * Default: DefaultDecorator()
78 | *
79 | * @param decorator
80 | * The decorator
81 | * @return This builder
82 | * @see DefaultDecorator
83 | * @since 0.7
84 | */
85 | public Builder setDecorator(Decorator decorator) {
86 | this.decorator = decorator;
87 | return this;
88 | }
89 | }
90 |
91 | /**
92 | * Creates a new Builder instance.
93 | *
94 | * @return A new Builder instance.
95 | */
96 | public static Builder builder() {
97 | return new Builder();
98 | }
99 |
100 | final String encoding;
101 | final Decorator decorator;
102 |
103 | Configuration(String encoding, Decorator decorator) {
104 | this.encoding = encoding;
105 | this.decorator = decorator;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/kmark/LineType.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Koji Lin
3 | * Copyright (C) 2011 René Jeschke
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package io.kaif.mobile.kmark;
18 |
19 | /**
20 | * Line type enumeration.
21 | *
22 | * @author René Jeschke
23 | */
24 | enum LineType {
25 | /**
26 | * Empty line.
27 | */
28 | EMPTY,
29 | /**
30 | * Undefined content.
31 | */
32 | OTHER,
33 | /**
34 | * A list.
35 | */
36 | ULIST, OLIST,
37 | /**
38 | * Fenced code block start/end
39 | */
40 | FENCED_CODE,
41 | /**
42 | * A block quote.
43 | */
44 | BQUOTE
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/kmark/LinkRef.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Koji Lin
3 | * Copyright (C) 2011 René Jeschke
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package io.kaif.mobile.kmark;
18 |
19 | /**
20 | * A markdown link reference.
21 | *
22 | * @author René Jeschke
23 | */
24 | class LinkRef {
25 |
26 | /**
27 | * reference sequence
28 | */
29 | public final int seqNumber;
30 |
31 | /**
32 | * The link.
33 | */
34 | public final String link;
35 | /**
36 | * The optional comment/title.
37 | */
38 | public String title;
39 |
40 | /**
41 | * Constructor.
42 | *
43 | * @param link
44 | * The link.
45 | * @param title
46 | * The title (may be null
).
47 | */
48 | public LinkRef(final int seqNumber, final String link, final String title) {
49 | this.seqNumber = seqNumber;
50 | this.link = link;
51 | this.title = title;
52 | }
53 |
54 | /**
55 | * @see Object#toString()
56 | */
57 | @Override
58 | public String toString() {
59 | return this.link + " \"" + this.title + "\"";
60 | }
61 |
62 | public boolean hasHttpScheme() {
63 | return link.startsWith("http://") || link.startsWith("https://");
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/kmark/MarkToken.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Koji Lin
3 | * Copyright (C) 2011 René Jeschke
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package io.kaif.mobile.kmark;
18 |
19 | /**
20 | * Markdown token enumeration.
21 | *
22 | * @author René Jeschke
23 | */
24 | enum MarkToken {
25 | /**
26 | * No token.
27 | */
28 | NONE,
29 | /**
30 | * *
31 | */
32 | EM_STAR, // x*x
33 | /**
34 | * _
35 | */
36 | EM_UNDERSCORE, // x_x
37 | /**
38 | * **
39 | */
40 | STRONG_STAR, // x**x
41 | /**
42 | * __
43 | */
44 | STRONG_UNDERSCORE, // x__x
45 | /**
46 | * ~~
47 | */
48 | STRIKE, // x~~x
49 | /**
50 | * `
51 | */
52 | CODE_SINGLE, // `
53 | /**
54 | * ``
55 | */
56 | CODE_DOUBLE, // ``
57 | /**
58 | * [
59 | */
60 | LINK, // [
61 | /**
62 | * \
63 | */
64 | ESCAPE, // \x
65 | /**
66 | * Extended: ^
67 | */
68 | SUPER, // ^
69 | /**
70 | * Extended: /u/NAME_PATTERN
71 | */
72 | USER,
73 | /**
74 | * Extended: /z/ZONE_PATTERN
75 | */
76 | ZONE,
77 |
78 | BR,
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/kmark/Utils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Koji Lin
3 | * Copyright (C) 2011 René Jeschke
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package io.kaif.mobile.kmark;
18 |
19 | /**
20 | * Utilities.
21 | *
22 | * @author René Jeschke
23 | */
24 | class Utils {
25 |
26 | /**
27 | * Skips spaces in the given String.
28 | *
29 | * @param in
30 | * Input String.
31 | * @param start
32 | * Starting position.
33 | * @return The new position or -1 if EOL has been reached.
34 | */
35 | public final static int skipSpaces(final String in, final int start) {
36 | int pos = start;
37 | while (pos < in.length() && (in.charAt(pos) == ' ' || in.charAt(pos) == '\n')) {
38 | pos++;
39 | }
40 | return pos < in.length() ? pos : -1;
41 | }
42 |
43 | /**
44 | * Reads a markdown link ID.
45 | *
46 | * @param out
47 | * The StringBuilder to write to.
48 | * @param in
49 | * Input String.
50 | * @param start
51 | * Starting position.
52 | * @return The new position or -1 if this is no valid markdown link ID.
53 | */
54 | public final static int readMdLinkId(final StringBuilder out, final String in, final int start) {
55 | int pos = start;
56 | int counter = 1;
57 | while (pos < in.length()) {
58 | final char ch = in.charAt(pos);
59 | boolean endReached = false;
60 | switch (ch) {
61 | case '\n':
62 | out.append(' ');
63 | break;
64 | case '[':
65 | counter++;
66 | out.append(ch);
67 | break;
68 | case ']':
69 | counter--;
70 | if (counter == 0) {
71 | endReached = true;
72 | } else {
73 | out.append(ch);
74 | }
75 | break;
76 | default:
77 | out.append(ch);
78 | break;
79 | }
80 | if (endReached) {
81 | break;
82 | }
83 | pos++;
84 | }
85 |
86 | return (pos == in.length()) ? -1 : pos;
87 | }
88 |
89 | /**
90 | * Reads characters until the end character is encountered, ignoring escape
91 | * sequences.
92 | *
93 | * @param out
94 | * The StringBuilder to write to.
95 | * @param in
96 | * The Input String.
97 | * @param start
98 | * Starting position.
99 | * @param end
100 | * End characters.
101 | * @return The new position or -1 if no 'end' char was found.
102 | */
103 | public final static int readRawUntil(final StringBuilder out,
104 | final String in,
105 | final int start,
106 | final char end) {
107 | int pos = start;
108 | while (pos < in.length()) {
109 | final char ch = in.charAt(pos);
110 | if (ch == end) {
111 | break;
112 | }
113 | out.append(ch);
114 | pos++;
115 | }
116 |
117 | return (pos == in.length()) ? -1 : pos;
118 | }
119 |
120 | /**
121 | * Removes trailing `
and trims spaces.
122 | *
123 | * @param fenceLine
124 | * Fenced code block starting line
125 | * @return Rest of the line after trimming and backtick removal
126 | * @since 0.7
127 | */
128 | public final static String getMetaFromFence(String fenceLine) {
129 | for (int i = 0; i < fenceLine.length(); i++) {
130 | final char c = fenceLine.charAt(i);
131 | if (!Character.isWhitespace(c) && c != '`' && c != '~' && c != '%') {
132 | return fenceLine.substring(i).trim();
133 | }
134 | }
135 | return "";
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/kmark/text/BulletSpan2.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.kmark.text;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Paint;
5 | import android.graphics.Path;
6 | import android.text.Layout;
7 | import android.text.Spanned;
8 | import android.text.style.LeadingMarginSpan;
9 |
10 | public class BulletSpan2 implements LeadingMarginSpan {
11 | private int leading;
12 | private final int gapWidth;
13 | private final int bulletRadius;
14 |
15 | private static Path sBulletPath = null;
16 |
17 | public BulletSpan2(int leading, int gapWidth, int bulletRadius) {
18 | this.leading = leading;
19 | this.gapWidth = gapWidth;
20 | this.bulletRadius = bulletRadius;
21 | }
22 |
23 | public int getLeadingMargin(boolean first) {
24 | return leading + (2 * bulletRadius + gapWidth);
25 | }
26 |
27 | public void drawLeadingMargin(Canvas c,
28 | Paint p,
29 | int x,
30 | int dir,
31 | int top,
32 | int baseline,
33 | int bottom,
34 | CharSequence text,
35 | int start,
36 | int end,
37 | boolean first,
38 | Layout l) {
39 | if (((Spanned) text).getSpanStart(this) == start) {
40 | Paint.Style style = p.getStyle();
41 | p.setStyle(Paint.Style.FILL);
42 |
43 | if (c.isHardwareAccelerated()) {
44 | if (sBulletPath == null) {
45 | sBulletPath = new Path();
46 | // Bullet is slightly better to avoid aliasing artifacts on mdpi devices.
47 | sBulletPath.addCircle(0.0f, 0.0f, 1.2f * bulletRadius, Path.Direction.CW);
48 | }
49 |
50 | c.save();
51 | c.translate(x + dir * bulletRadius + leading, (top + bottom) / 2.0f);
52 | c.drawPath(sBulletPath, p);
53 | c.restore();
54 | } else {
55 | c.drawCircle(x + dir * bulletRadius + leading, (top + bottom) / 2.0f, bulletRadius, p);
56 | }
57 |
58 | p.setStyle(style);
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/kmark/text/CodeBlockSpan.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.kmark.text;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.Paint;
5 | import android.text.style.LineBackgroundSpan;
6 |
7 | public class CodeBlockSpan implements LineBackgroundSpan {
8 | private int color;
9 |
10 | public CodeBlockSpan(int color) {
11 | this.color = color;
12 | }
13 |
14 | @Override
15 | public void drawBackground(Canvas c,
16 | Paint p,
17 | int left,
18 | int right,
19 | int top,
20 | int baseline,
21 | int bottom,
22 | CharSequence text,
23 | int start,
24 | int end,
25 | int lnum) {
26 | int oldcolor = p.getColor();
27 | p.setColor(color);
28 | c.drawRect(left, top, right, bottom, p);
29 | p.setColor(oldcolor);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/kmark/text/SuperscriptSpan2.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.kmark.text;
2 |
3 | import android.support.annotation.NonNull;
4 | import android.text.TextPaint;
5 | import android.text.style.SuperscriptSpan;
6 |
7 | public class SuperscriptSpan2 extends SuperscriptSpan {
8 | @Override
9 | public void updateDrawState(@NonNull TextPaint tp) {
10 | tp.setTextSize(tp.getTextSize() * 0.75f);
11 | tp.baselineShift += (int) (tp.ascent() / 2);
12 | }
13 |
14 | @Override
15 | public void updateMeasureState(@NonNull TextPaint tp) {
16 | tp.setTextSize(tp.getTextSize() * 0.75f);
17 | tp.baselineShift += (int) (tp.ascent() / 2);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/model/Article.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.model;
2 |
3 | import java.io.Serializable;
4 | import java.util.Date;
5 |
6 | public class Article implements Serializable {
7 |
8 | public enum ArticleType {
9 | EXTERNAL_LINK, SPEAK
10 | }
11 |
12 | private final String zone;
13 |
14 | private final String zoneTitle;
15 |
16 | private final String articleId;
17 |
18 | private final String title;
19 |
20 | private final Date createTime;
21 |
22 | private final String link;
23 |
24 | private final String content;
25 |
26 | private final ArticleType articleType;
27 |
28 | private final String authorName;
29 |
30 | private final long upVote;
31 |
32 | private final long debateCount;
33 |
34 | public Article(String zone,
35 | String zoneTitle,
36 | String articleId,
37 | String title,
38 | Date createTime,
39 | String link,
40 | String content,
41 | ArticleType articleType,
42 | String authorName,
43 | long upVote,
44 | long debateCount) {
45 | this.zone = zone;
46 | this.zoneTitle = zoneTitle;
47 | this.articleId = articleId;
48 | this.title = title;
49 | this.createTime = createTime;
50 | this.link = link;
51 | this.content = content;
52 | this.articleType = articleType;
53 | this.authorName = authorName;
54 | this.upVote = upVote;
55 | this.debateCount = debateCount;
56 | }
57 |
58 | public String getZone() {
59 | return zone;
60 | }
61 |
62 | public String getZoneTitle() {
63 | return zoneTitle;
64 | }
65 |
66 | public String getArticleId() {
67 | return articleId;
68 | }
69 |
70 | public String getTitle() {
71 | return title;
72 | }
73 |
74 | public Date getCreateTime() {
75 | return createTime;
76 | }
77 |
78 | public String getLink() {
79 | return link;
80 | }
81 |
82 | public String getContent() {
83 | return content;
84 | }
85 |
86 | public ArticleType getArticleType() {
87 | return articleType;
88 | }
89 |
90 | public String getAuthorName() {
91 | return authorName;
92 | }
93 |
94 | public long getUpVote() {
95 | return upVote;
96 | }
97 |
98 | public long getDebateCount() {
99 | return debateCount;
100 | }
101 |
102 | @Override
103 | public boolean equals(Object o) {
104 | if (this == o) {
105 | return true;
106 | }
107 | if (o == null || getClass() != o.getClass()) {
108 | return false;
109 | }
110 |
111 | Article article = (Article) o;
112 |
113 | return articleId.equals(article.articleId);
114 |
115 | }
116 |
117 | @Override
118 | public int hashCode() {
119 | return articleId.hashCode();
120 | }
121 |
122 | @Override
123 | public String toString() {
124 | return "Article{" +
125 | "zone='" + zone + '\'' +
126 | ", zoneTitle='" + zoneTitle + '\'' +
127 | ", articleId='" + articleId + '\'' +
128 | ", title='" + title + '\'' +
129 | ", createTime=" + createTime +
130 | ", link='" + link + '\'' +
131 | ", content='" + content + '\'' +
132 | ", articleType=" + articleType +
133 | ", authorName='" + authorName + '\'' +
134 | ", upVote=" + upVote +
135 | ", debateCount=" + debateCount +
136 | '}';
137 | }
138 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/model/Debate.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.model;
2 |
3 | import java.util.Date;
4 |
5 | public class Debate {
6 | private final String articleId;
7 |
8 | private final String debateId;
9 |
10 | private final String zone;
11 |
12 | private final String parentDebateId;
13 |
14 | private final int level;
15 |
16 | private final String content;
17 |
18 | private final String debaterName;
19 |
20 | private final long upVote;
21 |
22 | private final long downVote;
23 |
24 | private final Date createTime;
25 |
26 | private final Date lastUpdateTime;
27 |
28 | public Debate(String articleId,
29 | String debateId,
30 | String zone,
31 | String parentDebateId,
32 | int level,
33 | String content,
34 | String debaterName,
35 | long upVote,
36 | long downVote,
37 | Date createTime,
38 | Date lastUpdateTime) {
39 | this.articleId = articleId;
40 | this.debateId = debateId;
41 | this.zone = zone;
42 | this.parentDebateId = parentDebateId;
43 | this.level = level;
44 | this.content = content;
45 | this.debaterName = debaterName;
46 | this.upVote = upVote;
47 | this.downVote = downVote;
48 | this.createTime = createTime;
49 | this.lastUpdateTime = lastUpdateTime;
50 | }
51 |
52 | @Override
53 | public String toString() {
54 | return "Debate{" +
55 | "articleId='" + articleId + '\'' +
56 | ", debateId='" + debateId + '\'' +
57 | ", zone='" + zone + '\'' +
58 | ", parentDebateId='" + parentDebateId + '\'' +
59 | ", level=" + level +
60 | ", content='" + content + '\'' +
61 | ", debaterName='" + debaterName + '\'' +
62 | ", upVote=" + upVote +
63 | ", downVote=" + downVote +
64 | ", createTime=" + createTime +
65 | ", lastUpdateTime=" + lastUpdateTime +
66 | '}';
67 | }
68 |
69 | @Override
70 | public boolean equals(Object o) {
71 | if (this == o) {
72 | return true;
73 | }
74 | if (o == null || getClass() != o.getClass()) {
75 | return false;
76 | }
77 |
78 | Debate debate = (Debate) o;
79 |
80 | return debateId.equals(debate.debateId);
81 |
82 | }
83 |
84 | @Override
85 | public int hashCode() {
86 | return debateId.hashCode();
87 | }
88 |
89 | public String getArticleId() {
90 | return articleId;
91 | }
92 |
93 | public String getDebateId() {
94 | return debateId;
95 | }
96 |
97 | public String getZone() {
98 | return zone;
99 | }
100 |
101 | public String getParentDebateId() {
102 | return parentDebateId;
103 | }
104 |
105 | public int getLevel() {
106 | return level;
107 | }
108 |
109 | public String getContent() {
110 | return content;
111 | }
112 |
113 | public String getDebaterName() {
114 | return debaterName;
115 | }
116 |
117 | public long getUpVote() {
118 | return upVote;
119 | }
120 |
121 | public long getDownVote() {
122 | return downVote;
123 | }
124 |
125 | public Date getCreateTime() {
126 | return createTime;
127 | }
128 |
129 | public Date getLastUpdateTime() {
130 | return lastUpdateTime;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/model/DebateNode.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.model;
2 |
3 | import java.util.List;
4 |
5 | public class DebateNode {
6 |
7 | private final Debate debate;
8 |
9 | private final List children;
10 |
11 | public DebateNode(Debate debate, List children) {
12 | this.debate = debate;
13 | this.children = children;
14 | }
15 |
16 | public Debate getDebate() {
17 | return debate;
18 | }
19 |
20 | public List getChildren() {
21 | return children;
22 | }
23 |
24 | @Override
25 | public boolean equals(Object o) {
26 | if (this == o) {
27 | return true;
28 | }
29 | if (o == null || getClass() != o.getClass()) {
30 | return false;
31 | }
32 |
33 | DebateNode that = (DebateNode) o;
34 |
35 | if (debate != null ? !debate.equals(that.debate) : that.debate != null) {
36 | return false;
37 | }
38 | return children.equals(that.children);
39 |
40 | }
41 |
42 | @Override
43 | public int hashCode() {
44 | int result = debate != null ? debate.hashCode() : 0;
45 | result = 31 * result + children.hashCode();
46 | return result;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/model/FeedAsset.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.model;
2 |
3 | import java.io.Serializable;
4 | import java.util.Date;
5 |
6 | import android.support.annotation.Nullable;
7 |
8 | public class FeedAsset implements Serializable {
9 | enum AssetType {
10 | DEBATE_FROM_REPLY
11 | }
12 |
13 | private String assetId;
14 |
15 | private AssetType assetType;
16 |
17 | private Date createTime;
18 |
19 | private boolean acknowledged;
20 |
21 | @Nullable
22 | private Debate debate;
23 |
24 | public FeedAsset(String assetId,
25 | AssetType assetType,
26 | Date createTime,
27 | boolean acknowledged,
28 | @Nullable Debate debate) {
29 | this.assetId = assetId;
30 | this.assetType = assetType;
31 | this.createTime = createTime;
32 | this.acknowledged = acknowledged;
33 | this.debate = debate;
34 | }
35 |
36 | public String getAssetId() {
37 | return assetId;
38 | }
39 |
40 | public AssetType getAssetType() {
41 | return assetType;
42 | }
43 |
44 | public Date getCreateTime() {
45 | return createTime;
46 | }
47 |
48 | public boolean isAcknowledged() {
49 | return acknowledged;
50 | }
51 |
52 | @Nullable
53 | public Debate getDebate() {
54 | return debate;
55 | }
56 |
57 | @Override
58 | public boolean equals(Object o) {
59 | if (this == o) {
60 | return true;
61 | }
62 | if (o == null || getClass() != o.getClass()) {
63 | return false;
64 | }
65 |
66 | FeedAsset feedAsset = (FeedAsset) o;
67 |
68 | return assetId.equals(feedAsset.assetId);
69 |
70 | }
71 |
72 | @Override
73 | public int hashCode() {
74 | return assetId.hashCode();
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/model/LocalDebate.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.model;
2 |
3 | public class LocalDebate {
4 |
5 | private final String articleId;
6 |
7 | private final String localDebateId;
8 |
9 | private final String parentDebateId;
10 |
11 | private final int level;
12 |
13 | private final String content;
14 |
15 | public LocalDebate(String articleId,
16 | String localDebateId,
17 | String parentDebateId,
18 | int level,
19 | String content) {
20 | this.articleId = articleId;
21 | this.localDebateId = localDebateId;
22 | this.parentDebateId = parentDebateId;
23 | this.level = level;
24 | this.content = content;
25 | }
26 |
27 | public String getArticleId() {
28 | return articleId;
29 | }
30 |
31 | public String getLocalDebateId() {
32 | return localDebateId;
33 | }
34 |
35 | public String getParentDebateId() {
36 | return parentDebateId;
37 | }
38 |
39 | public int getLevel() {
40 | return level;
41 | }
42 |
43 | public String getContent() {
44 | return content;
45 | }
46 |
47 | @Override
48 | public boolean equals(Object o) {
49 | if (this == o) {
50 | return true;
51 | }
52 | if (o == null || getClass() != o.getClass()) {
53 | return false;
54 | }
55 |
56 | LocalDebate that = (LocalDebate) o;
57 |
58 | return localDebateId.equals(that.localDebateId);
59 |
60 | }
61 |
62 | @Override
63 | public int hashCode() {
64 | return localDebateId.hashCode();
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/model/Vote.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.model;
2 |
3 | import java.io.Serializable;
4 | import java.util.Date;
5 |
6 | public class Vote implements Serializable {
7 | public enum VoteState {
8 | UP(1), DOWN(-1), EMPTY(0);
9 |
10 | private int score;
11 |
12 | VoteState(int score) {
13 | this.score = score;
14 | }
15 |
16 | public int delta(VoteState prevVoteState) {
17 | return score - prevVoteState.score;
18 | }
19 | }
20 |
21 | private final String targetId;
22 | private final VoteState voteState;
23 | private final Date updateTime;
24 |
25 | public Vote(String targetId, VoteState voteState, Date updateTime) {
26 | this.targetId = targetId;
27 | this.voteState = voteState;
28 | this.updateTime = updateTime;
29 | }
30 |
31 | public boolean matches(String targetId) {
32 | return this.targetId.equals(targetId);
33 | }
34 |
35 | public VoteState getVoteState() {
36 | return voteState;
37 | }
38 |
39 | @Override
40 | public boolean equals(Object o) {
41 | if (this == o) {
42 | return true;
43 | }
44 | if (o == null || getClass() != o.getClass()) {
45 | return false;
46 | }
47 |
48 | Vote vote = (Vote) o;
49 |
50 | if (!targetId.equals(vote.targetId)) {
51 | return false;
52 | }
53 | if (voteState != vote.voteState) {
54 | return false;
55 | }
56 | return updateTime.equals(vote.updateTime);
57 |
58 | }
59 |
60 | @Override
61 | public int hashCode() {
62 | int result = targetId.hashCode();
63 | result = 31 * result + voteState.hashCode();
64 | result = 31 * result + updateTime.hashCode();
65 | return result;
66 | }
67 |
68 | @Override
69 | public String toString() {
70 | return "Vote{" +
71 | "targetId='" + targetId + '\'' +
72 | ", voteState=" + voteState +
73 | ", updateTime=" + updateTime +
74 | '}';
75 | }
76 |
77 | public static Vote abstain(String id) {
78 | return new Vote(id, VoteState.EMPTY, new Date(0));
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/model/Zone.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.model;
2 |
3 | public class Zone {
4 | private String name;
5 | private String aliasName;
6 |
7 | public Zone(String name, String aliasName) {
8 | this.name = name;
9 | this.aliasName = aliasName;
10 | }
11 |
12 | public String getName() {
13 | return name;
14 | }
15 |
16 | public String getAliasName() {
17 | return aliasName;
18 | }
19 |
20 | @Override
21 | public boolean equals(Object o) {
22 | if (this == o) {
23 | return true;
24 | }
25 | if (o == null || getClass() != o.getClass()) {
26 | return false;
27 | }
28 |
29 | Zone zone = (Zone) o;
30 |
31 | return name.equals(zone.name);
32 |
33 | }
34 |
35 | @Override
36 | public int hashCode() {
37 | return name.hashCode();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/model/exception/DomainException.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.model.exception;
2 |
3 | public abstract class DomainException extends Exception {
4 | }
5 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/model/exception/DuplicateArticleUrlException.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.model.exception;
2 |
3 | public class DuplicateArticleUrlException extends DomainException{
4 | }
5 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/model/oauth/AccessTokenInfo.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.model.oauth;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 |
5 | public class AccessTokenInfo {
6 |
7 | @SerializedName("token_type")
8 | private final String tokenType;
9 |
10 | @SerializedName("access_token")
11 | private final String accessToken;
12 |
13 | private final String scope;
14 |
15 | public AccessTokenInfo(String tokenType, String accessToken, String scope) {
16 | this.tokenType = tokenType;
17 | this.accessToken = accessToken;
18 | this.scope = scope;
19 | }
20 |
21 | public String getAuthorization() {
22 | return tokenType + " " + accessToken;
23 | }
24 |
25 | @Override
26 | public String toString() {
27 | return "AccessTokenInfo{" +
28 | "tokenType='" + tokenType + '\'' +
29 | ", accessToken='" + accessToken + '\'' +
30 | ", scope='" + scope + '\'' +
31 | '}';
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/model/oauth/AccessTokenManager.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.model.oauth;
2 |
3 | import javax.inject.Inject;
4 | import javax.inject.Singleton;
5 |
6 | import com.google.gson.Gson;
7 |
8 | import android.content.SharedPreferences;
9 |
10 | @Singleton
11 | public class AccessTokenManager {
12 |
13 | public static final String ACCESS_TOKEN_KEY = "ACCESS_TOKEN";
14 |
15 | SharedPreferences preference;
16 |
17 | Gson gson;
18 |
19 | @Inject
20 | public AccessTokenManager(SharedPreferences preference, Gson gson) {
21 | this.gson = gson;
22 | this.preference = preference;
23 | }
24 |
25 | public boolean hasAccount() {
26 | return preference.contains(ACCESS_TOKEN_KEY);
27 | }
28 |
29 | public void signOut() {
30 | preference.edit().remove(ACCESS_TOKEN_KEY).apply();
31 | }
32 |
33 | public void saveAccount(AccessTokenInfo accessTokenInfo) {
34 | preference.edit().putString(ACCESS_TOKEN_KEY, gson.toJson(accessTokenInfo)).apply();
35 | }
36 |
37 | public AccessTokenInfo findAccount() {
38 | return gson.fromJson(preference.getString(ACCESS_TOKEN_KEY, null), AccessTokenInfo.class);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/retrofit/MethodInfo.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.retrofit;
2 |
3 | import java.lang.reflect.InvocationTargetException;
4 | import java.lang.reflect.Method;
5 |
6 | public class MethodInfo {
7 |
8 | private Method method;
9 |
10 | private boolean isObservable;
11 |
12 | private boolean isGetMethod;
13 |
14 | public MethodInfo(Method method, boolean isObservable, boolean isGetMethod) {
15 | this.method = method;
16 | this.isObservable = isObservable;
17 | this.isGetMethod = isGetMethod;
18 | }
19 |
20 | public Object invoke(Object receiver, Object[] args)
21 | throws InvocationTargetException, IllegalAccessException {
22 | return method.invoke(receiver, args);
23 | }
24 |
25 | public boolean canRetry() {
26 | return isObservable && isGetMethod;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/retrofit/RetrofitRetryStaleProxy.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.retrofit;
2 |
3 | import java.lang.reflect.Proxy;
4 |
5 | import javax.inject.Inject;
6 |
7 | import retrofit2.Retrofit;
8 |
9 | /**
10 | * TODO
11 | * Generate this using annotation processor
12 | */
13 | public class RetrofitRetryStaleProxy {
14 |
15 | public static class RetrofitHolder {
16 |
17 | @Inject
18 | Retrofit retrofit;
19 |
20 | public RetrofitHolder(Retrofit retrofit) {
21 | this.retrofit = retrofit;
22 | }
23 |
24 | public T create(Class serviceClass) {
25 | return retrofit.create(serviceClass);
26 | }
27 | }
28 |
29 | @Inject
30 | RetrofitHolder retrofitHolder;
31 |
32 | public RetrofitRetryStaleProxy(RetrofitHolder retrofitHolder) {
33 | this.retrofitHolder = retrofitHolder;
34 | }
35 |
36 | public T create(Class serviceClass) {
37 | try {
38 | return serviceClass.cast(Proxy.newProxyInstance(serviceClass.getClassLoader(),
39 | new Class[]{serviceClass},
40 | new RetryStaleHandler(retrofitHolder.create(Class.forName(serviceClass.getName()
41 | + "$$RetryStale")))));
42 | } catch (ClassNotFoundException e) {
43 | return retrofitHolder.create(serviceClass);
44 | }
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/retrofit/RetryStaleHandler.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.retrofit;
2 |
3 | import java.lang.reflect.InvocationHandler;
4 | import java.lang.reflect.Method;
5 | import java.util.concurrent.ConcurrentHashMap;
6 |
7 | import retrofit2.adapter.rxjava.HttpException;
8 | import retrofit2.http.GET;
9 | import rx.Observable;
10 | import rx.functions.Func1;
11 |
12 | /**
13 | * TODO
14 | * Generate this using annotation processor
15 | */
16 | class RetryStaleHandler implements InvocationHandler {
17 |
18 | private ConcurrentHashMap methodCache;
19 |
20 | private ConcurrentHashMap retryMethodCache;
21 |
22 | private Object target;
23 |
24 | public RetryStaleHandler(Object target) {
25 | this.target = target;
26 | this.methodCache = new ConcurrentHashMap<>();
27 | this.retryMethodCache = new ConcurrentHashMap<>();
28 | }
29 |
30 | @Override
31 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
32 | MethodInfo methodInfo = getTargetMethodInfo(method);
33 | if (methodInfo.canRetry()) {
34 | Observable result = (Observable) methodInfo.invoke(target, args);
35 | return result.onErrorResumeNext((Func1) throwable -> {
36 | if (throwable instanceof HttpException) {
37 | try {
38 | return (Observable) getTargetCacheMethod(method).invoke(target, args);
39 | } catch (Exception e) {
40 | return Observable.error(throwable);
41 | }
42 | }
43 | return Observable.error(throwable);
44 | });
45 | }
46 |
47 | return methodInfo.invoke(target, args);
48 | }
49 |
50 | private MethodInfo getTargetMethodInfo(Method method) throws NoSuchMethodException {
51 | MethodInfo methodInfo = methodCache.get(method);
52 | if (methodInfo == null) {
53 | Method targetMethod = target.getClass()
54 | .getMethod(method.getName(), method.getParameterTypes());
55 | methodInfo = new MethodInfo(targetMethod,
56 | method.getReturnType().isAssignableFrom(Observable.class),
57 | method.isAnnotationPresent(GET.class));
58 |
59 | methodCache.putIfAbsent(method, methodInfo);
60 | }
61 | return methodInfo;
62 | }
63 |
64 | private Method getTargetCacheMethod(Method method) throws NoSuchMethodException {
65 | Method targetMethod = retryMethodCache.get(method);
66 | if (targetMethod == null) {
67 | targetMethod = target.getClass()
68 | .getMethod(method.getName() + "$$RetryStale", method.getParameterTypes());
69 | retryMethodCache.putIfAbsent(method, targetMethod);
70 | }
71 | return targetMethod;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/service/AccountService.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.service;
2 |
3 | public interface AccountService {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/service/ArticleService.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.service;
2 |
3 | import java.util.List;
4 |
5 | import io.kaif.mobile.model.Article;
6 | import retrofit2.http.Body;
7 | import retrofit2.http.GET;
8 | import retrofit2.http.PUT;
9 | import retrofit2.http.Path;
10 | import retrofit2.http.Query;
11 | import rx.Observable;
12 |
13 | public interface ArticleService {
14 |
15 | class ExternalLinkEntry {
16 | String url;
17 | String title;
18 | String zone;
19 |
20 | public ExternalLinkEntry(String url, String title, String zone) {
21 | this.url = url;
22 | this.title = title;
23 | this.zone = zone;
24 | }
25 |
26 | @Override
27 | public boolean equals(Object o) {
28 | if (this == o) {
29 | return true;
30 | }
31 | if (o == null || getClass() != o.getClass()) {
32 | return false;
33 | }
34 |
35 | ExternalLinkEntry that = (ExternalLinkEntry) o;
36 |
37 | if (!url.equals(that.url)) {
38 | return false;
39 | }
40 | if (!title.equals(that.title)) {
41 | return false;
42 | }
43 | return zone.equals(that.zone);
44 |
45 | }
46 |
47 | @Override
48 | public int hashCode() {
49 | int result = url.hashCode();
50 | result = 31 * result + title.hashCode();
51 | result = 31 * result + zone.hashCode();
52 | return result;
53 | }
54 | }
55 |
56 | @PUT("/v1/article/external-link")
57 | Observable createExternalLink(@Body ExternalLinkEntry externalLinkEntry);
58 |
59 | @GET("/v1/article/hot")
60 | Observable> listHotArticles(@Query("start-article-id") String startArticleId);
61 |
62 | @GET("/v1/article/zone/{zone}/external-link/exist")
63 | Observable exist(@Path("zone") String zone, @Query("url") String url);
64 |
65 | @GET("/v1/article/latest")
66 | Observable> listLatestArticles(@Query("start-article-id") String startArticleId);
67 |
68 | @GET("/v1/article/{articleId}")
69 | Observable loadArticle(@Path("articleId") String articleId);
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/service/CommaSeparatedParam.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.service;
2 |
3 | import java.util.Arrays;
4 | import java.util.List;
5 |
6 | import io.kaif.mobile.util.StringUtils;
7 |
8 | public class CommaSeparatedParam {
9 |
10 | private String[] params;
11 |
12 | public static CommaSeparatedParam of(List params) {
13 | return new CommaSeparatedParam(params.toArray(new String[params.size()]));
14 | }
15 |
16 | public CommaSeparatedParam(String[] params) {
17 | this.params = params;
18 | }
19 |
20 | @Override
21 | public String toString() {
22 | return StringUtils.join(",", params);
23 | }
24 |
25 | @Override
26 | public boolean equals(Object o) {
27 | if (this == o) {
28 | return true;
29 | }
30 | if (o == null || getClass() != o.getClass()) {
31 | return false;
32 | }
33 |
34 | CommaSeparatedParam that = (CommaSeparatedParam) o;
35 |
36 | // Probably incorrect - comparing Object[] arrays with Arrays.equals
37 | return Arrays.equals(params, that.params);
38 |
39 | }
40 |
41 | @Override
42 | public int hashCode() {
43 | return Arrays.hashCode(params);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/service/DebateService.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.service;
2 |
3 | import java.util.List;
4 |
5 | import io.kaif.mobile.model.Debate;
6 | import io.kaif.mobile.model.DebateNode;
7 | import retrofit2.http.Body;
8 | import retrofit2.http.GET;
9 | import retrofit2.http.PUT;
10 | import retrofit2.http.Path;
11 | import retrofit2.http.Query;
12 | import rx.Observable;
13 |
14 | public interface DebateService {
15 |
16 | class CreateDebateEntry {
17 | String articleId;
18 | String parentDebateId;
19 | String content;
20 |
21 | public CreateDebateEntry(String articleId, String parentDebateId, String content) {
22 | this.articleId = articleId;
23 | this.parentDebateId = parentDebateId;
24 | this.content = content;
25 | }
26 |
27 | @Override
28 | public String toString() {
29 | return "CreateDebateEntry{" +
30 | "articleId='" + articleId + '\'' +
31 | ", parentDebateId='" + parentDebateId + '\'' +
32 | ", content='" + content + '\'' +
33 | '}';
34 | }
35 |
36 | @Override
37 | public boolean equals(Object o) {
38 | if (this == o) {
39 | return true;
40 | }
41 | if (o == null || getClass() != o.getClass()) {
42 | return false;
43 | }
44 |
45 | CreateDebateEntry that = (CreateDebateEntry) o;
46 |
47 | if (!articleId.equals(that.articleId)) {
48 | return false;
49 | }
50 | if (parentDebateId != null
51 | ? !parentDebateId.equals(that.parentDebateId)
52 | : that.parentDebateId != null) {
53 | return false;
54 | }
55 | return content.equals(that.content);
56 |
57 | }
58 |
59 | @Override
60 | public int hashCode() {
61 | int result = articleId.hashCode();
62 | result = 31 * result + (parentDebateId != null ? parentDebateId.hashCode() : 0);
63 | result = 31 * result + content.hashCode();
64 | return result;
65 | }
66 | }
67 |
68 | @GET("/v1/debate/latest")
69 | Observable> listLatestDebates(@Query("start-debate-id") String startDebateId);
70 |
71 |
72 | @GET("/v1/debate/article/{articleId}/tree")
73 | Observable getDebateTree(@Path("articleId") String articleId);
74 |
75 | @PUT("/v1/debate")
76 | Observable debate(@Body CreateDebateEntry createDebateEntry);
77 | }
78 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/service/FeedService.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.service;
2 |
3 | import java.util.List;
4 |
5 | import io.kaif.mobile.model.FeedAsset;
6 | import retrofit2.http.Body;
7 | import retrofit2.http.GET;
8 | import retrofit2.http.POST;
9 | import retrofit2.http.Query;
10 | import rx.Observable;
11 |
12 | public interface FeedService {
13 |
14 | class AcknowledgeEntry {
15 | String assetId;
16 |
17 | public AcknowledgeEntry(String assetId) {
18 | this.assetId = assetId;
19 | }
20 |
21 | @Override
22 | public boolean equals(Object o) {
23 | if (this == o) {
24 | return true;
25 | }
26 | if (o == null || getClass() != o.getClass()) {
27 | return false;
28 | }
29 |
30 | AcknowledgeEntry that = (AcknowledgeEntry) o;
31 |
32 | return assetId.equals(that.assetId);
33 |
34 | }
35 |
36 | @Override
37 | public int hashCode() {
38 | return assetId.hashCode();
39 | }
40 | }
41 |
42 | @POST("/v1/feed/acknowledge")
43 | Observable acknowledge(@Body AcknowledgeEntry acknowledgeEntry);
44 |
45 | @GET("/v1/feed/news")
46 | Observable> news(@Query("start-asset-id") String startAssetId);
47 |
48 | @GET("/v1/feed/news-unread-count")
49 | Observable newsUnreadCount();
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/service/OauthService.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.service;
2 |
3 | import io.kaif.mobile.model.oauth.AccessTokenInfo;
4 | import retrofit2.http.Field;
5 | import retrofit2.http.FormUrlEncoded;
6 | import retrofit2.http.POST;
7 | import rx.Observable;
8 |
9 | public interface OauthService {
10 |
11 | @FormUrlEncoded
12 | @POST("/oauth/access-token")
13 | Observable getAccessToken(@Field("client_id") String clientId,
14 | @Field("client_secret") String clientSecret,
15 | @Field("code") String code,
16 | @Field("redirect_uri") String redirectUri,
17 | @Field("grant_type") String grantType);
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/service/VoteService.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.service;
2 |
3 | import java.util.List;
4 |
5 | import io.kaif.mobile.model.Vote;
6 | import retrofit2.http.Body;
7 | import retrofit2.http.GET;
8 | import retrofit2.http.POST;
9 | import retrofit2.http.Query;
10 | import rx.Observable;
11 |
12 | public interface VoteService {
13 |
14 | class VoteArticleEntry {
15 | String articleId;
16 | Vote.VoteState voteState;
17 |
18 | public VoteArticleEntry(String articleId, Vote.VoteState voteState) {
19 | this.articleId = articleId;
20 | this.voteState = voteState;
21 | }
22 |
23 | @Override
24 | public boolean equals(Object o) {
25 | if (this == o) {
26 | return true;
27 | }
28 | if (o == null || getClass() != o.getClass()) {
29 | return false;
30 | }
31 |
32 | VoteArticleEntry that = (VoteArticleEntry) o;
33 |
34 | if (!articleId.equals(that.articleId)) {
35 | return false;
36 | }
37 | return voteState == that.voteState;
38 |
39 | }
40 |
41 | @Override
42 | public int hashCode() {
43 | int result = articleId.hashCode();
44 | result = 31 * result + voteState.hashCode();
45 | return result;
46 | }
47 | }
48 |
49 | class VoteDebateEntry {
50 | String debateId;
51 | Vote.VoteState voteState;
52 |
53 | public VoteDebateEntry(String debateId, Vote.VoteState voteState) {
54 | this.debateId = debateId;
55 | this.voteState = voteState;
56 | }
57 |
58 | @Override
59 | public boolean equals(Object o) {
60 | if (this == o) {
61 | return true;
62 | }
63 | if (o == null || getClass() != o.getClass()) {
64 | return false;
65 | }
66 |
67 | VoteDebateEntry that = (VoteDebateEntry) o;
68 |
69 | if (!debateId.equals(that.debateId)) {
70 | return false;
71 | }
72 | return voteState == that.voteState;
73 |
74 | }
75 |
76 | @Override
77 | public int hashCode() {
78 | int result = debateId.hashCode();
79 | result = 31 * result + voteState.hashCode();
80 | return result;
81 | }
82 | }
83 |
84 | @GET("/v1/vote/article")
85 | Observable> listArticleVotes(
86 | @Query(value = "article-id") CommaSeparatedParam articleIds);
87 |
88 | @GET("/v1/vote/debate")
89 | Observable> listDebateVotes(
90 | @Query(value = "debate-id") CommaSeparatedParam articleIds);
91 |
92 | @POST("/v1/vote/article")
93 | Observable voteArticle(@Body VoteArticleEntry voteArticleEntry);
94 |
95 | @POST("/v1/vote/debate")
96 | Observable voteDebate(@Body VoteDebateEntry voteDebateEntry);
97 | }
98 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/service/ZoneService.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.service;
2 |
3 | import java.util.List;
4 |
5 | import io.kaif.mobile.model.Zone;
6 | import retrofit2.http.GET;
7 | import rx.Observable;
8 |
9 | public interface ZoneService {
10 |
11 | @GET("/v1/zone/all")
12 | Observable> listAll();
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/util/StringUtils.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.util;
2 |
3 | public class StringUtils {
4 |
5 | /**
6 | * copy from TextUtils
7 | */
8 | public static String join(CharSequence delimiter, Object[] tokens) {
9 | StringBuilder sb = new StringBuilder();
10 | boolean firstTime = true;
11 | for (Object token : tokens) {
12 | if (firstTime) {
13 | firstTime = false;
14 | } else {
15 | sb.append(delimiter);
16 | }
17 | sb.append(token);
18 | }
19 | return sb.toString();
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/util/UtilModule.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.util;
2 |
3 | import javax.inject.Singleton;
4 |
5 | import com.google.gson.Gson;
6 | import com.squareup.leakcanary.LeakCanary;
7 | import com.squareup.leakcanary.RefWatcher;
8 |
9 | import android.app.Application;
10 | import android.content.Context;
11 | import android.content.SharedPreferences;
12 | import android.net.ConnectivityManager;
13 | import android.preference.PreferenceManager;
14 | import dagger.Module;
15 | import dagger.Provides;
16 |
17 | @Module
18 | public class UtilModule {
19 |
20 | private final Application application;
21 |
22 | public UtilModule(Application application) {
23 | this.application = application;
24 | }
25 |
26 | @Provides
27 | @Singleton
28 | Context provideApplicationContext() {
29 | return this.application;
30 | }
31 |
32 | @Provides
33 | @Singleton
34 | Gson provideGson() {
35 | return new Gson();
36 | }
37 |
38 | @Provides
39 | @Singleton
40 | SharedPreferences provideSharedPreferences() {
41 | return PreferenceManager.getDefaultSharedPreferences(application);
42 | }
43 |
44 | @Provides
45 | @Singleton
46 | ConnectivityManager provideConnectivityManager() {
47 | return (ConnectivityManager) application.getSystemService(Context.CONNECTIVITY_SERVICE);
48 | }
49 |
50 | @Provides
51 | @Singleton
52 | RefWatcher provideRefWatcher() {
53 | return LeakCanary.install(application);
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/HomeActivity.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.support.v4.app.Fragment;
6 | import android.support.v7.widget.Toolbar;
7 | import android.widget.Toast;
8 |
9 | import javax.inject.Inject;
10 |
11 | import butterknife.BindView;
12 | import butterknife.ButterKnife;
13 | import io.kaif.mobile.KaifApplication;
14 | import io.kaif.mobile.R;
15 | import io.kaif.mobile.app.BaseActivity;
16 | import io.kaif.mobile.event.account.SignOutEvent;
17 | import io.kaif.mobile.view.daemon.AccountDaemon;
18 |
19 | public class HomeActivity extends BaseActivity {
20 |
21 | @Inject
22 | AccountDaemon accountDaemon;
23 |
24 | @BindView(R.id.tool_bar)
25 | Toolbar toolbar;
26 |
27 | @Override
28 | protected void onCreate(Bundle savedInstanceState) {
29 | super.onCreate(savedInstanceState);
30 | setContentView(R.layout.activity_home);
31 | ButterKnife.bind(this);
32 | KaifApplication.getInstance().beans().inject(this);
33 | setSupportActionBar(toolbar);
34 |
35 | if (!accountDaemon.hasAccount()) {
36 | showLoginActivityAndFinish();
37 | return;
38 | }
39 |
40 | bind(accountDaemon.getSubject(SignOutEvent.class)).subscribe(accountEvent -> {
41 | Toast.makeText(this, R.string.sign_out_success, Toast.LENGTH_SHORT).show();
42 | showLoginActivityAndFinish();
43 | });
44 |
45 | if (savedInstanceState == null) {
46 | final Fragment fragment = getSupportFragmentManager().findFragmentByTag("hot");
47 | if (fragment == null) {
48 | getSupportFragmentManager().beginTransaction()
49 | .replace(R.id.container, HomeFragment.newInstance(), "hot")
50 | .commit();
51 | }
52 | }
53 | }
54 |
55 | private void showLoginActivityAndFinish() {
56 | startActivity(new Intent(this, LoginActivity.class));
57 | finish();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/HomeFragment.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view;
2 |
3 | import android.os.Bundle;
4 | import android.support.design.widget.TabLayout;
5 | import android.support.v4.view.ViewPager;
6 | import android.view.LayoutInflater;
7 | import android.view.Menu;
8 | import android.view.MenuInflater;
9 | import android.view.MenuItem;
10 | import android.view.View;
11 | import android.view.ViewGroup;
12 |
13 | import javax.inject.Inject;
14 |
15 | import butterknife.BindView;
16 | import butterknife.ButterKnife;
17 | import io.kaif.mobile.KaifApplication;
18 | import io.kaif.mobile.R;
19 | import io.kaif.mobile.app.BaseFragment;
20 | import io.kaif.mobile.view.daemon.AccountDaemon;
21 | import io.kaif.mobile.view.daemon.FeedDaemon;
22 | import io.kaif.mobile.view.drawable.NewsFeedBadgeDrawable;
23 |
24 | public class HomeFragment extends BaseFragment {
25 |
26 | public static HomeFragment newInstance() {
27 | return new HomeFragment();
28 | }
29 |
30 | @BindView(R.id.sliding_tabs)
31 | TabLayout slidingTabLayout;
32 |
33 | @BindView(R.id.view_pager)
34 | ViewPager viewPager;
35 |
36 | @Inject
37 | AccountDaemon accountDaemon;
38 |
39 | @Inject
40 | FeedDaemon feedDaemon;
41 |
42 | private NewsFeedBadgeDrawable newsFeedBadgeDrawable;
43 |
44 | public HomeFragment() {
45 | }
46 |
47 | @Override
48 | public void onCreate(Bundle savedInstanceState) {
49 | super.onCreate(savedInstanceState);
50 | KaifApplication.getInstance().beans().inject(this);
51 | setHasOptionsMenu(true);
52 | newsFeedBadgeDrawable = new NewsFeedBadgeDrawable(getResources());
53 | }
54 |
55 | @Override
56 | public void onResume() {
57 | super.onResume();
58 | bind(feedDaemon.newsUnreadCount()).subscribe(newsFeedBadgeDrawable::changeCount, ignoreEx -> {
59 |
60 | });
61 | }
62 |
63 | @Override
64 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
65 | inflater.inflate(R.menu.menu_home, menu);
66 | MenuItem newsFeedAction = menu.findItem(R.id.action_news_feed);
67 | newsFeedAction.setIcon(newsFeedBadgeDrawable);
68 | super.onCreateOptionsMenu(menu, inflater);
69 | }
70 |
71 | @Override
72 | public boolean onOptionsItemSelected(MenuItem item) {
73 | int id = item.getItemId();
74 | if (id == R.id.action_sign_out) {
75 | accountDaemon.signOut();
76 | return true;
77 | }
78 | if (id == R.id.action_news_feed) {
79 | startActivity(new NewsFeedActivity.NewsFeedActivityIntent(getActivity()));
80 | return true;
81 | }
82 | return super.onOptionsItemSelected(item);
83 | }
84 |
85 | @Override
86 | public View onCreateView(LayoutInflater inflater,
87 | ViewGroup container,
88 | Bundle savedInstanceState) {
89 | View view = inflater.inflate(R.layout.fragment_home, container, false);
90 | ButterKnife.bind(this, view);
91 |
92 | viewPager.setAdapter(new HomePagerAdapter(getActivity(), getFragmentManager()));
93 | viewPager.setOffscreenPageLimit(2);
94 | slidingTabLayout.setBackgroundColor(getResources().getColor(R.color.kaif_blue));
95 | slidingTabLayout.setupWithViewPager(viewPager);
96 | viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(slidingTabLayout));
97 |
98 | return view;
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/HomePagerAdapter.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view;
2 |
3 | import android.content.Context;
4 | import android.support.v4.app.Fragment;
5 | import android.support.v4.app.FragmentManager;
6 | import android.support.v4.app.FragmentPagerAdapter;
7 | import io.kaif.mobile.R;
8 |
9 | public class HomePagerAdapter extends FragmentPagerAdapter {
10 |
11 | private Context context;
12 |
13 | public HomePagerAdapter(Context context, FragmentManager fm) {
14 | super(fm);
15 | this.context = context;
16 | }
17 |
18 | @Override
19 | public int getCount() {
20 | return 3;
21 | }
22 |
23 | @Override
24 | public Fragment getItem(int position) {
25 | if (position == 0) {
26 | return ArticlesFragment.newInstance(true);
27 | } else if (position == 1) {
28 | return ArticlesFragment.newInstance(false);
29 | } else {
30 | return LatestDebatesFragment.newInstance();
31 | }
32 | }
33 |
34 | @Override
35 | public CharSequence getPageTitle(int position) {
36 | switch (position) {
37 | case 0:
38 | return context.getString(R.string.hot);
39 | case 1:
40 | return context.getString(R.string.latest);
41 | case 2:
42 | return context.getString(R.string.debate);
43 | default:
44 | return null;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/LatestDebateListAdapter.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view;
2 |
3 | import android.content.Context;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.text.format.DateUtils;
6 | import android.view.LayoutInflater;
7 | import android.view.View;
8 | import android.view.ViewGroup;
9 | import android.widget.TextView;
10 |
11 | import java.util.ArrayList;
12 | import java.util.List;
13 |
14 | import butterknife.BindView;
15 | import butterknife.ButterKnife;
16 | import io.kaif.mobile.R;
17 | import io.kaif.mobile.kmark.KmarkProcessor;
18 | import io.kaif.mobile.view.viewmodel.DebateViewModel;
19 | import io.kaif.mobile.view.widget.ClickableSpanTouchListener;
20 |
21 | public class LatestDebateListAdapter extends RecyclerView.Adapter {
22 |
23 | static class DebateViewHolder extends RecyclerView.ViewHolder {
24 |
25 | @BindView(R.id.content)
26 | public TextView content;
27 | @BindView(R.id.last_update_time)
28 | public TextView lastUpdateTime;
29 | @BindView(R.id.vote_score)
30 | public TextView voteScore;
31 | @BindView(R.id.debater_name)
32 | public TextView debaterName;
33 | @BindView(R.id.zone)
34 | public TextView zone;
35 |
36 | public DebateViewHolder(View itemView) {
37 | super(itemView);
38 | ButterKnife.bind(this, itemView);
39 | content.setOnTouchListener(new ClickableSpanTouchListener());
40 | }
41 |
42 | public void update(DebateViewModel debateViewModel) {
43 | final Context context = itemView.getContext();
44 | debaterName.setText(debateViewModel.getDebaterName());
45 | content.setText(KmarkProcessor.process(context, debateViewModel.getContent()));
46 | lastUpdateTime.setText(DateUtils.getRelativeTimeSpanString(debateViewModel.getLastUpdateTime()
47 | .getTime(), System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE));
48 | voteScore.setText(String.valueOf(debateViewModel.getVoteScore()));
49 | zone.setText(itemView.getContext().getString(R.string.zone_path, debateViewModel.getZone()));
50 | }
51 |
52 | }
53 |
54 | private final List debates;
55 |
56 | private boolean hasNextPage;
57 |
58 | public LatestDebateListAdapter() {
59 | this.debates = new ArrayList<>();
60 | }
61 |
62 | @Override
63 | public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
64 | final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
65 | if (viewType == R.layout.item_loading) {
66 | return new RecyclerView.ViewHolder(view) {
67 | };
68 | }
69 | return new DebateViewHolder(view);
70 | }
71 |
72 | @Override
73 | public int getItemViewType(int position) {
74 | if (position >= debates.size()) {
75 | return R.layout.item_loading;
76 | }
77 | return R.layout.item_debate_latest;
78 | }
79 |
80 | @Override
81 | public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
82 | if (position >= debates.size()) {
83 | return;
84 | }
85 | DebateViewModel debateVm = debates.get(position);
86 | DebateViewHolder debateViewHolder = (DebateViewHolder) holder;
87 | debateViewHolder.update(debateVm);
88 | }
89 |
90 | @Override
91 | public int getItemCount() {
92 | return debates.size() + (hasNextPage ? 1 : 0);
93 | }
94 |
95 | public void refresh(List debates) {
96 | this.debates.clear();
97 | this.debates.addAll(debates);
98 | hasNextPage = !debates.isEmpty();
99 | notifyDataSetChanged();
100 | }
101 |
102 | public void addAll(List debates) {
103 | if (debates.isEmpty()) {
104 | hasNextPage = false;
105 | return;
106 | }
107 | this.debates.addAll(debates);
108 | notifyItemRangeInserted(this.debates.size() - debates.size(), debates.size());
109 | }
110 |
111 | public String getLastDebateId() {
112 | return debates.get(debates.size() - 1).getDebateId();
113 | }
114 |
115 | public DebateViewModel findItem(int position) {
116 | if (position >= debates.size()) {
117 | return null;
118 | }
119 | return debates.get(position);
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/LoginActivity.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view;
2 |
3 | import android.content.Intent;
4 | import android.graphics.Rect;
5 | import android.net.Uri;
6 | import android.os.Bundle;
7 | import android.view.View;
8 | import android.widget.Button;
9 | import android.widget.ProgressBar;
10 | import android.widget.TextView;
11 | import android.widget.Toast;
12 |
13 | import javax.inject.Inject;
14 |
15 | import butterknife.BindView;
16 | import butterknife.ButterKnife;
17 | import io.kaif.mobile.KaifApplication;
18 | import io.kaif.mobile.R;
19 | import io.kaif.mobile.app.BaseActivity;
20 | import io.kaif.mobile.view.daemon.AccountDaemon;
21 | import io.kaif.mobile.view.graphics.drawable.Triangle;
22 | import io.kaif.mobile.view.util.Views;
23 |
24 | public class LoginActivity extends BaseActivity {
25 |
26 | @Inject
27 | AccountDaemon accountDaemon;
28 |
29 | @BindView(R.id.sign_in)
30 | Button signInBtn;
31 |
32 | @BindView(R.id.sign_in_progress)
33 | ProgressBar signInProgress;
34 |
35 | @BindView(R.id.sign_in_title)
36 | TextView signInTitle;
37 |
38 | @BindView(R.id.title)
39 | TextView title;
40 |
41 | @Override
42 | protected void onCreate(Bundle savedInstanceState) {
43 | super.onCreate(savedInstanceState);
44 | setContentView(R.layout.activity_login);
45 |
46 | ButterKnife.bind(this);
47 | KaifApplication.getInstance().beans().inject(this);
48 |
49 | int triangleSize = (int) -title.getPaint().ascent();
50 | Triangle triangle = new Triangle(getResources().getColor(R.color.vote_state_up), false);
51 | triangle.setBounds(new Rect(0, 0, triangleSize, triangleSize));
52 | title.setCompoundDrawables(triangle, null, null, null);
53 | title.setCompoundDrawablePadding((int) Views.convertDpToPixel(16, this));
54 |
55 | signInBtn.setOnClickListener(v -> startActivity(accountDaemon.createOauthPageIntent()));
56 | }
57 |
58 | @Override
59 | protected void onNewIntent(Intent intent) {
60 | super.onNewIntent(intent);
61 | setIntent(intent);
62 | }
63 |
64 | @Override
65 | protected void onResume() {
66 | super.onResume();
67 | final Uri data = getIntent().getData();
68 | if (data == null) {
69 | return;
70 | }
71 | getIntent().setData(null);
72 |
73 | signInProgress.setVisibility(View.VISIBLE);
74 | signInTitle.setVisibility(View.VISIBLE);
75 | signInBtn.setVisibility(View.GONE);
76 | bind(accountDaemon.accessToken(data.getQueryParameter("code"), data.getQueryParameter("state")))
77 | .subscribe(aVoid -> {
78 | Toast.makeText(this, R.string.sign_in_success, Toast.LENGTH_SHORT).show();
79 | Intent intent = new Intent(this, HomeActivity.class);
80 | startActivity(intent);
81 | finish();
82 | }, throwable -> {
83 | Toast.makeText(this, throwable.toString(), Toast.LENGTH_SHORT).show();
84 | signInProgress.setVisibility(View.GONE);
85 | signInTitle.setVisibility(View.GONE);
86 | signInBtn.setVisibility(View.VISIBLE);
87 | });
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/NewsFeedActivity.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.os.Bundle;
7 | import android.support.v4.app.NavUtils;
8 | import android.support.v4.app.TaskStackBuilder;
9 | import android.support.v7.widget.Toolbar;
10 | import android.view.MenuItem;
11 |
12 | import butterknife.BindView;
13 | import butterknife.ButterKnife;
14 | import io.kaif.mobile.KaifApplication;
15 | import io.kaif.mobile.R;
16 | import io.kaif.mobile.app.BaseActivity;
17 |
18 | public class NewsFeedActivity extends BaseActivity {
19 |
20 | //TODO
21 | @SuppressLint("ParcelCreator")
22 | static class NewsFeedActivityIntent extends Intent {
23 |
24 | NewsFeedActivityIntent(Context context) {
25 | super(context, NewsFeedActivity.class);
26 | }
27 |
28 | }
29 |
30 | @BindView(R.id.tool_bar)
31 | Toolbar toolbar;
32 |
33 | @Override
34 | protected void onCreate(Bundle savedInstanceState) {
35 | super.onCreate(savedInstanceState);
36 | setContentView(R.layout.activity_news_feed);
37 | ButterKnife.bind(this);
38 | KaifApplication.getInstance().beans().inject(this);
39 | setSupportActionBar(toolbar);
40 | setTitle(R.string.news_feed);
41 |
42 | //noinspection ConstantConditions
43 | getSupportActionBar().setDisplayHomeAsUpEnabled(true);
44 | }
45 |
46 | @Override
47 | public boolean onOptionsItemSelected(MenuItem item) {
48 | switch (item.getItemId()) {
49 | case android.R.id.home:
50 | Intent upIntent = NavUtils.getParentActivityIntent(this);
51 | if (NavUtils.shouldUpRecreateTask(this, upIntent)) {
52 | TaskStackBuilder.create(this).addNextIntentWithParentStack(upIntent).startActivities();
53 | } else {
54 | upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
55 | NavUtils.navigateUpTo(this, upIntent);
56 | }
57 | return true;
58 | }
59 | return super.onOptionsItemSelected(item);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/NewsFeedActivityFragment.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.support.v4.widget.SwipeRefreshLayout;
6 | import android.support.v7.widget.LinearLayoutManager;
7 | import android.support.v7.widget.RecyclerView;
8 | import android.text.TextUtils;
9 | import android.view.LayoutInflater;
10 | import android.view.View;
11 | import android.view.ViewGroup;
12 |
13 | import java.util.List;
14 |
15 | import javax.inject.Inject;
16 |
17 | import butterknife.BindView;
18 | import butterknife.ButterKnife;
19 | import io.kaif.mobile.KaifApplication;
20 | import io.kaif.mobile.R;
21 | import io.kaif.mobile.app.BaseFragment;
22 | import io.kaif.mobile.view.daemon.FeedDaemon;
23 | import io.kaif.mobile.view.viewmodel.FeedAssetViewModel;
24 | import io.kaif.mobile.view.widget.OnScrollToLastListener;
25 | import rx.Observable;
26 |
27 | public class NewsFeedActivityFragment extends BaseFragment {
28 |
29 | @BindView(R.id.debate_list)
30 | RecyclerView debateListView;
31 |
32 | @BindView(R.id.pull_to_refresh)
33 | SwipeRefreshLayout pullToRefreshLayout;
34 |
35 | @Inject
36 | FeedDaemon feedDaemon;
37 |
38 | private NewsFeedListAdapter adapter;
39 |
40 | public NewsFeedActivityFragment() {
41 | }
42 |
43 | @Override
44 | public void onCreate(Bundle savedInstanceState) {
45 | super.onCreate(savedInstanceState);
46 | KaifApplication.getInstance().beans().inject(this);
47 | }
48 |
49 | @Override
50 | public View onCreateView(LayoutInflater inflater,
51 | ViewGroup container,
52 | Bundle savedInstanceState) {
53 | View view = inflater.inflate(R.layout.fragment_news_feed, container, false);
54 | ButterKnife.bind(this, view);
55 | setupView();
56 | fillContent();
57 | return view;
58 | }
59 |
60 | private void fillContent() {
61 | reload();
62 | }
63 |
64 | private void setupView() {
65 | final LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getActivity());
66 | debateListView.setLayoutManager(linearLayoutManager);
67 | adapter = new NewsFeedListAdapter();
68 | adapter.setOnItemClickListener(debateViewModel -> {
69 | Intent intent = DebatesActivity.DebatesActivityIntent.create(getActivity(), debateViewModel);
70 | startActivity(intent);
71 | });
72 |
73 | debateListView.setAdapter(adapter);
74 | debateListView.getItemAnimator().setChangeDuration(120);
75 | debateListView.addOnScrollListener(new OnScrollToLastListener() {
76 | private boolean loadingNextPage = false;
77 |
78 | @Override
79 | public void onScrollToLast() {
80 | if (loadingNextPage) {
81 | return;
82 | }
83 | loadingNextPage = true;
84 | bind(listFeedAssets(adapter.getLastAssetId())).subscribe(adapter::addAll, throwable -> {
85 | }, () -> loadingNextPage = false);
86 | }
87 | });
88 | pullToRefreshLayout.setOnRefreshListener(this::reload);
89 | }
90 |
91 | private void reload() {
92 | pullToRefreshLayout.setRefreshing(true);
93 | bind(listFeedAssets(null)).subscribe(adapter::refresh, throwable -> {
94 | }, () -> pullToRefreshLayout.setRefreshing(false));
95 | }
96 |
97 | private Observable> listFeedAssets(String feedAssetId) {
98 | if (TextUtils.isEmpty(feedAssetId)) {
99 | return feedDaemon.listAndAcknowledgeIfRequired();
100 | }
101 | return feedDaemon.listNewsFeed(feedAssetId);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/animation/VoteAnimation.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.animation;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorSet;
5 | import android.animation.ArgbEvaluator;
6 | import android.animation.ObjectAnimator;
7 | import android.animation.ValueAnimator;
8 | import android.content.Context;
9 | import android.graphics.PorterDuff;
10 | import android.view.View;
11 | import android.widget.TextView;
12 | import io.kaif.mobile.R;
13 |
14 | public class VoteAnimation {
15 |
16 | private static ValueAnimator colorChangeAnimation(View view, int from, int to) {
17 | ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), from, to);
18 | colorAnimation.addUpdateListener(animation -> {
19 | final Integer color = (Integer) animation.getAnimatedValue();
20 | view.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
21 | view.invalidate();
22 | });
23 | return colorAnimation;
24 | }
25 |
26 | public static Animator voteUpAnimation(View view) {
27 | Context context = view.getContext();
28 | ValueAnimator colorAnimation = colorChangeAnimation(view,
29 | context.getResources().getColor(R.color.vote_state_empty),
30 | context.getResources().getColor(R.color.vote_state_up));
31 | AnimatorSet animatorSet = new AnimatorSet();
32 | final ObjectAnimator rotateAnimation = ObjectAnimator.ofFloat(view, "rotation", 0, 360f);
33 | animatorSet.play(colorAnimation).with(rotateAnimation);
34 | animatorSet.setDuration(300);
35 | return animatorSet;
36 | }
37 |
38 | public static Animator voteDownAnimation(View view) {
39 | Context context = view.getContext();
40 | ValueAnimator colorAnimation = colorChangeAnimation(view,
41 | context.getResources().getColor(R.color.vote_state_empty),
42 | context.getResources().getColor(R.color.vote_state_down));
43 | AnimatorSet animatorSet = new AnimatorSet();
44 | final ObjectAnimator rotateAnimation = ObjectAnimator.ofFloat(view, "rotation", 0, -360f);
45 | animatorSet.play(colorAnimation).with(rotateAnimation);
46 | animatorSet.setDuration(300);
47 | return animatorSet;
48 | }
49 |
50 | public static Animator voteUpReverseAnimation(View view) {
51 | Context context = view.getContext();
52 | ValueAnimator colorAnimation = colorChangeAnimation(view,
53 | context.getResources().getColor(R.color.vote_state_up),
54 | context.getResources().getColor(R.color.vote_state_empty));
55 | AnimatorSet animatorSet = new AnimatorSet();
56 |
57 | final ObjectAnimator rotateAnimation = ObjectAnimator.ofFloat(view, "rotation", 360, 0f);
58 | animatorSet.play(colorAnimation).with(rotateAnimation);
59 | animatorSet.setDuration(300);
60 | return animatorSet;
61 | }
62 |
63 | public static Animator voteDownReverseAnimation(View view) {
64 | Context context = view.getContext();
65 | ValueAnimator colorAnimation = colorChangeAnimation(view,
66 | context.getResources().getColor(R.color.vote_state_down),
67 | context.getResources().getColor(R.color.vote_state_empty));
68 | AnimatorSet animatorSet = new AnimatorSet();
69 |
70 | final ObjectAnimator rotateAnimation = ObjectAnimator.ofFloat(view, "rotation", -360, 0f);
71 | animatorSet.play(colorAnimation).with(rotateAnimation);
72 | animatorSet.setDuration(300);
73 | return animatorSet;
74 | }
75 |
76 | private static ValueAnimator colorChangeAnimation(TextView view, int from, int to) {
77 | ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), from, to);
78 | colorAnimation.addUpdateListener(animation -> {
79 | final Integer color = (Integer) animation.getAnimatedValue();
80 | view.setTextColor(color);
81 | });
82 | colorAnimation.setDuration(300);
83 | return colorAnimation;
84 | }
85 |
86 | public static ValueAnimator voteUpTextColorAnimation(TextView view) {
87 | Context context = view.getContext();
88 |
89 | return colorChangeAnimation(view,
90 | context.getResources().getColor(R.color.vote_state_empty),
91 | context.getResources().getColor(R.color.vote_state_up));
92 | }
93 |
94 | public static ValueAnimator voteUpReverseTextColorAnimation(TextView view) {
95 | Context context = view.getContext();
96 |
97 | return colorChangeAnimation(view,
98 | context.getResources().getColor(R.color.vote_state_up),
99 | context.getResources().getColor(R.color.vote_state_empty));
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/daemon/AccountDaemon.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.daemon;
2 |
3 | import java.util.UUID;
4 |
5 | import javax.inject.Inject;
6 | import javax.inject.Singleton;
7 |
8 | import android.content.Intent;
9 | import android.net.Uri;
10 | import android.text.TextUtils;
11 | import io.kaif.mobile.config.ApiConfiguration;
12 | import io.kaif.mobile.event.EventPublishSubject;
13 | import io.kaif.mobile.event.account.AccountEvent;
14 | import io.kaif.mobile.event.account.SignInSuccessEvent;
15 | import io.kaif.mobile.event.account.SignOutEvent;
16 | import io.kaif.mobile.model.oauth.AccessTokenManager;
17 | import io.kaif.mobile.service.OauthService;
18 | import rx.Observable;
19 |
20 | @Singleton
21 | public class AccountDaemon {
22 |
23 | private final EventPublishSubject subject;
24 |
25 | private final OauthService oauthService;
26 |
27 | private final AccessTokenManager accessTokenManager;
28 |
29 | private final ApiConfiguration apiConfiguration;
30 |
31 | private String state;
32 |
33 | @Inject
34 | AccountDaemon(OauthService oauthService,
35 | AccessTokenManager accessTokenManager,
36 | ApiConfiguration apiConfiguration) {
37 | this.oauthService = oauthService;
38 | this.accessTokenManager = accessTokenManager;
39 | this.apiConfiguration = apiConfiguration;
40 | this.subject = new EventPublishSubject<>();
41 | }
42 |
43 | public Observable getSubject(Class>... classes) {
44 | return subject.getSubject(classes);
45 | }
46 |
47 | public Observable getSubject(Class clazz) {
48 | return subject.getSubject(clazz);
49 | }
50 |
51 | public Intent createOauthPageIntent() {
52 | state = UUID.randomUUID().toString();
53 | final Uri uri = Uri.parse(apiConfiguration.getEndPoint())
54 | .buildUpon()
55 | .appendPath("oauth")
56 | .appendPath("authorize")
57 | .appendQueryParameter("client_id", apiConfiguration.getClientId())
58 | .appendQueryParameter("response_type", "code")
59 | .appendQueryParameter("scope", "public feed article vote user debate")
60 | .appendQueryParameter("state", state)
61 | .appendQueryParameter("redirect_uri", apiConfiguration.getRedirectUri())
62 | .build();
63 | Intent intent = new Intent(Intent.ACTION_VIEW);
64 | intent.setData(uri);
65 | return intent;
66 | }
67 |
68 | public boolean hasAccount() {
69 | return accessTokenManager.hasAccount();
70 | }
71 |
72 | public void signOut() {
73 | accessTokenManager.signOut();
74 | subject.onNext(new SignOutEvent());
75 | }
76 |
77 | public Observable accessToken(String code, String state) {
78 | if (!TextUtils.equals(this.state, state)) {
79 | return Observable.error(new IllegalStateException("receive a malicious response"));
80 | }
81 | this.state = null;
82 | return oauthService.getAccessToken(apiConfiguration.getClientId(),
83 | apiConfiguration.getClientSecret(),
84 | code,
85 | apiConfiguration.getRedirectUri(),
86 | "authorization_code").map(accessTokenInfo -> {
87 | accessTokenManager.saveAccount(accessTokenInfo);
88 | subject.onNext(new SignInSuccessEvent());
89 | return null;
90 | });
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/daemon/FeedDaemon.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.daemon;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import javax.inject.Inject;
7 | import javax.inject.Singleton;
8 |
9 | import io.kaif.mobile.IgnoreAllSubscriber;
10 | import io.kaif.mobile.model.FeedAsset;
11 | import io.kaif.mobile.service.FeedService;
12 | import io.kaif.mobile.view.viewmodel.FeedAssetViewModel;
13 | import rx.Observable;
14 |
15 | @Singleton
16 | public class FeedDaemon {
17 |
18 | private final FeedService feedService;
19 |
20 | @Inject
21 | FeedDaemon(FeedService feedService) {
22 | this.feedService = feedService;
23 | }
24 |
25 | public Observable newsUnreadCount() {
26 | return feedService.newsUnreadCount();
27 | }
28 |
29 | public Observable> listAndAcknowledgeIfRequired() {
30 | return feedService.news(null).map(feedAssets -> {
31 | if (!feedAssets.isEmpty()) {
32 | feedService.acknowledge(new FeedService.AcknowledgeEntry(feedAssets.get(0).getAssetId()))
33 | .subscribe(new IgnoreAllSubscriber<>());
34 | }
35 | return mapToViewModel(feedAssets);
36 | });
37 | }
38 |
39 | private List mapToViewModel(List feedAssets) {
40 | List vms = new ArrayList<>();
41 | boolean isRead = false;
42 | for (int i = 0; i < feedAssets.size(); i++) {
43 | FeedAsset feedAsset = feedAssets.get(i);
44 | isRead |= feedAsset.isAcknowledged();
45 | vms.add(new FeedAssetViewModel(feedAsset, isRead));
46 | }
47 | return vms;
48 | }
49 |
50 | public Observable> listNewsFeed(String feedAssetId) {
51 | return feedService.news(feedAssetId).map(this::mapToViewModel);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/daemon/VoteDaemon.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.daemon;
2 |
3 | import javax.inject.Inject;
4 | import javax.inject.Singleton;
5 |
6 | import io.kaif.mobile.event.EventPublishSubject;
7 | import io.kaif.mobile.event.vote.VoteArticleSuccessEvent;
8 | import io.kaif.mobile.event.vote.VoteDebateSuccessEvent;
9 | import io.kaif.mobile.event.vote.VoteEvent;
10 | import io.kaif.mobile.model.Vote;
11 | import io.kaif.mobile.service.VoteService;
12 | import rx.Observable;
13 |
14 | @Singleton
15 | public class VoteDaemon {
16 |
17 | private final EventPublishSubject subject;
18 |
19 | private final VoteService voteService;
20 |
21 | public Observable getSubject(Class>... classes) {
22 | return subject.getSubject(classes);
23 | }
24 |
25 | public Observable getSubject(Class clazz) {
26 | return subject.getSubject(clazz);
27 | }
28 |
29 | @Inject
30 | VoteDaemon(VoteService voteService) {
31 | this.voteService = voteService;
32 | this.subject = new EventPublishSubject<>();
33 | }
34 |
35 | public void voteArticle(String articleId, Vote.VoteState prevState, Vote.VoteState voteState) {
36 | subject.onNext(new VoteArticleSuccessEvent(articleId, voteState));
37 | voteService.voteArticle(new VoteService.VoteArticleEntry(articleId, voteState))
38 | .subscribe(aVoid -> {
39 | //success do nothing
40 | }, throwable -> {
41 | subject.onNext(new VoteArticleSuccessEvent(articleId, prevState));
42 | });
43 | }
44 |
45 | public void voteDebate(String debateId, Vote.VoteState prevState, Vote.VoteState voteState) {
46 | subject.onNext(new VoteDebateSuccessEvent(debateId, voteState));
47 | voteService.voteDebate(new VoteService.VoteDebateEntry(debateId, voteState))
48 | .subscribe(aVoid -> {
49 | //success do nothing
50 | }, throwable -> {
51 | subject.onNext(new VoteDebateSuccessEvent(debateId, prevState));
52 | });
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/daemon/ZoneDaemon.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.daemon;
2 |
3 | import java.util.List;
4 |
5 | import javax.inject.Inject;
6 | import javax.inject.Singleton;
7 |
8 | import io.kaif.mobile.BuildConfig;
9 | import io.kaif.mobile.model.Zone;
10 | import io.kaif.mobile.service.ZoneService;
11 | import rx.Observable;
12 |
13 | @Singleton
14 | public class ZoneDaemon {
15 |
16 | private final ZoneService zoneService;
17 |
18 | @Inject
19 | ZoneDaemon(ZoneService zoneService) {
20 | this.zoneService = zoneService;
21 | }
22 |
23 | public Observable> listAll() {
24 |
25 | Observable> result = zoneService.listAll();
26 | if (BuildConfig.DEBUG) {
27 | return result.map(zones -> {
28 | zones.add(new Zone("test", "測試專區"));
29 | return zones;
30 | });
31 | }
32 | return result;
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/drawable/NewsFeedBadgeDrawable.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.drawable;
2 |
3 | import android.content.res.Resources;
4 | import android.graphics.Canvas;
5 | import android.graphics.ColorFilter;
6 | import android.graphics.Paint;
7 | import android.graphics.PixelFormat;
8 | import android.graphics.Rect;
9 | import android.graphics.drawable.Drawable;
10 | import android.support.v4.content.res.ResourcesCompat;
11 | import android.text.Layout;
12 | import android.text.StaticLayout;
13 | import android.text.TextPaint;
14 | import io.kaif.mobile.R;
15 | import io.kaif.mobile.view.util.Views;
16 |
17 | public class NewsFeedBadgeDrawable extends Drawable {
18 |
19 | private long count;
20 | private TextPaint textPaint;
21 | private Paint circlePaint;
22 | private final Resources resources;
23 | private final Drawable icon;
24 | private StaticLayout textLayout;
25 | private int countRadius;
26 |
27 | public NewsFeedBadgeDrawable(Resources resources) {
28 | this.resources = resources;
29 | icon = ResourcesCompat.getDrawable(resources, R.drawable.ic_notifications_white, null);
30 |
31 | textPaint = new TextPaint();
32 | textPaint.setTextSize((int) (11.0f * resources.getDisplayMetrics().density + 0.5f));
33 | textPaint.setColor(resources.getColor(android.R.color.white));
34 | textPaint.setAntiAlias(true);
35 |
36 | circlePaint = new Paint();
37 | circlePaint.setColor(0xffff0000);
38 | circlePaint.setAntiAlias(true);
39 |
40 | changeCount(0);
41 | }
42 |
43 | @Override
44 | public void draw(Canvas canvas) {
45 | icon.draw(canvas);
46 | if (count == 0) {
47 | return;
48 | }
49 | canvas.save();
50 | canvas.translate(Views.convertDpToPixel(24, resources), Views.convertDpToPixel(8, resources));
51 | canvas.drawCircle(0, 0, countRadius, circlePaint);
52 | canvas.translate(-textLayout.getWidth() / 2 - 1, -textLayout.getHeight() / 2 - 1);
53 | textLayout.draw(canvas);
54 | canvas.restore();
55 |
56 | }
57 |
58 | @Override
59 | public void setAlpha(int alpha) {
60 | icon.setAlpha(alpha);
61 | textPaint.setAlpha(alpha);
62 | circlePaint.setAlpha(alpha);
63 | }
64 |
65 | @Override
66 | public void setColorFilter(ColorFilter cf) {
67 | icon.setColorFilter(cf);
68 | textPaint.setColorFilter(cf);
69 | circlePaint.setColorFilter(cf);
70 | }
71 |
72 | @Override
73 | public int getIntrinsicHeight() {
74 | return (int) Views.convertDpToPixel(36, resources);
75 | }
76 |
77 | @Override
78 | public int getIntrinsicWidth() {
79 | return (int) Views.convertDpToPixel(36, resources);
80 | }
81 |
82 | @Override
83 | public int getOpacity() {
84 | return PixelFormat.TRANSLUCENT;
85 | }
86 |
87 | private String count() {
88 | return count > 10 ? "10+" : String.valueOf(count);
89 | }
90 |
91 | public final void changeCount(int count) {
92 | this.count = count;
93 | int textWidth = (int) (textPaint.measureText(count()));
94 | textLayout = new StaticLayout(count(),
95 | textPaint,
96 | textWidth,
97 | Layout.Alignment.ALIGN_CENTER,
98 | 1.0f,
99 | 0.0f,
100 | false);
101 | countRadius = (int) (Math.sqrt(Math.pow(textLayout.getWidth() / 2.0, 2)
102 | + Math.pow(textLayout.getHeight() / 2.0, 2)));
103 | }
104 |
105 | @Override
106 | protected void onBoundsChange(Rect bounds) {
107 | int size = (int) Views.convertDpToPixel(24, resources);
108 | int horizontalPadding = Math.max(0, (bounds.width() - size) / 2);
109 | int verticalPadding = Math.max(0, (bounds.height() - size) / 2);
110 |
111 | icon.setBounds(horizontalPadding,
112 | verticalPadding,
113 | Math.min(horizontalPadding + size, bounds.right),
114 | Math.min(verticalPadding + size, bounds.bottom));
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/graphics/drawable/LevelDrawable.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.graphics.drawable;
2 |
3 | import android.content.Context;
4 | import android.graphics.Canvas;
5 | import android.graphics.ColorFilter;
6 | import android.graphics.Paint;
7 | import android.graphics.PixelFormat;
8 | import android.graphics.Rect;
9 | import android.graphics.drawable.Drawable;
10 | import io.kaif.mobile.R;
11 | import io.kaif.mobile.view.util.Views;
12 |
13 | public class LevelDrawable extends Drawable {
14 |
15 | public static final int MAX_NESTED_LEVEL = 7;
16 | private Paint paint;
17 | private int alpha;
18 | private int innerLevel = 1;
19 | private int paddingDp;
20 | private int lineWidthDp;
21 | private int backgroundColor;
22 | private final int paddingVertical;
23 |
24 | public LevelDrawable(Context context, int innerLevel, int backgroundColor) {
25 | this.backgroundColor = backgroundColor;
26 | this.paddingDp = (int) Views.convertDpToPixel(12, context);
27 | this.paddingVertical = (int) Views.convertDpToPixel(4, context);
28 | this.lineWidthDp = (int) Views.convertDpToPixel(2, context);
29 | this.innerLevel = Math.min(innerLevel, MAX_NESTED_LEVEL);
30 | this.paint = new Paint();
31 | this.paint.setAntiAlias(true);
32 | this.paint.setColor(context.getResources().getColor(R.color.kaif_blue_light));
33 | this.paint.setStyle(Paint.Style.FILL);
34 | this.paint.setStrokeWidth(lineWidthDp);
35 | }
36 |
37 | @Override
38 | public int getOpacity() {
39 | return PixelFormat.TRANSLUCENT;
40 | }
41 |
42 | @Override
43 | public void draw(Canvas canvas) {
44 | canvas.drawColor(backgroundColor);
45 | for (int i = 1; i < innerLevel; ++i) {
46 | final int x = paddingDp * i - lineWidthDp;
47 | canvas.drawLine(x, 0, x, canvas.getHeight(), paint);
48 | }
49 | }
50 |
51 | @Override
52 | public boolean getPadding(Rect padding) {
53 | padding.set(paddingDp * (innerLevel - 1) + lineWidthDp * 2,
54 | paddingVertical,
55 | lineWidthDp,
56 | paddingVertical);
57 | return true;
58 | }
59 |
60 | @Override
61 | public void setAlpha(int alpha) {
62 | this.alpha = alpha;
63 | }
64 |
65 | @Override
66 | public int getAlpha() {
67 | return alpha;
68 | }
69 |
70 | @Override
71 | public void setColorFilter(ColorFilter cf) {
72 | paint.setColorFilter(cf);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/graphics/drawable/Triangle.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.graphics.drawable;
2 |
3 | import android.graphics.Canvas;
4 | import android.graphics.ColorFilter;
5 | import android.graphics.Paint;
6 | import android.graphics.Path;
7 | import android.graphics.PixelFormat;
8 | import android.graphics.Rect;
9 | import android.graphics.drawable.Drawable;
10 |
11 | public class Triangle extends Drawable {
12 |
13 | private Paint paint;
14 | private int alpha;
15 | Path triangle;
16 | private boolean reverse;
17 |
18 | public Triangle(int color) {
19 | this(color, false);
20 | }
21 |
22 | public Triangle(int color, boolean reverse) {
23 | this.reverse = reverse;
24 | this.triangle = new Path();
25 |
26 | this.paint = new Paint();
27 | this.paint.setAntiAlias(true);
28 | this.paint.setColor(color);
29 | this.paint.setStyle(Paint.Style.FILL);
30 | }
31 |
32 | @Override
33 | public int getOpacity() {
34 | return PixelFormat.OPAQUE;
35 | }
36 |
37 | @Override
38 | public void draw(Canvas canvas) {
39 | canvas.drawPath(triangle, this.paint);
40 | }
41 |
42 | @Override
43 | protected void onBoundsChange(Rect bounds) {
44 | triangle.reset();
45 | if (reverse) {
46 | triangle.moveTo(bounds.left + bounds.width() / 2f, bounds.bottom);
47 | triangle.lineTo(bounds.left + bounds.width(), bounds.top);
48 | triangle.lineTo(bounds.left, bounds.top);
49 | return;
50 | }
51 | triangle.moveTo(bounds.left + bounds.width() / 2f, bounds.top);
52 | triangle.lineTo(bounds.left + bounds.width(), bounds.bottom);
53 | triangle.lineTo(bounds.left, bounds.bottom);
54 | }
55 |
56 | @Override
57 | public void setAlpha(int alpha) {
58 | this.alpha = alpha;
59 | }
60 |
61 | @Override
62 | public int getAlpha() {
63 | return alpha;
64 | }
65 |
66 | @Override
67 | public void setColorFilter(ColorFilter cf) {
68 | paint.setColorFilter(cf);
69 | }
70 |
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/share/ShareArticleActivity.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.share;
2 |
3 | import android.os.Bundle;
4 | import android.support.v7.widget.Toolbar;
5 | import android.widget.Toast;
6 |
7 | import javax.inject.Inject;
8 |
9 | import butterknife.BindView;
10 | import butterknife.ButterKnife;
11 | import io.kaif.mobile.KaifApplication;
12 | import io.kaif.mobile.R;
13 | import io.kaif.mobile.app.BaseActivity;
14 | import io.kaif.mobile.view.daemon.AccountDaemon;
15 |
16 | public class ShareArticleActivity extends BaseActivity {
17 |
18 | @BindView(R.id.tool_bar)
19 | Toolbar toolbar;
20 |
21 | @Inject
22 | AccountDaemon accountDaemon;
23 |
24 | @Override
25 | public void onCreate(Bundle savedInstanceState) {
26 | super.onCreate(savedInstanceState);
27 | setContentView(R.layout.activity_share_article);
28 | ButterKnife.bind(this);
29 |
30 | KaifApplication.getInstance().beans().inject(this);
31 | setSupportActionBar(toolbar);
32 |
33 | if (!accountDaemon.hasAccount()) {
34 | Toast.makeText(this, R.string.not_sign_in_warning, Toast.LENGTH_SHORT).show();
35 | finish();
36 | }
37 |
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/util/Views.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.util;
2 |
3 | import android.content.Context;
4 | import android.content.res.Resources;
5 | import android.util.DisplayMetrics;
6 |
7 | public class Views {
8 | public static float convertDpToPixel(float dp, Context context) {
9 | return convertDpToPixel(dp, context.getResources());
10 | }
11 |
12 | public static float convertDpToPixel(float dp, Resources resources) {
13 |
14 | DisplayMetrics metrics = resources.getDisplayMetrics();
15 | return dp * (metrics.densityDpi / 160f);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/viewmodel/ArticleViewModel.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.viewmodel;
2 |
3 | import java.io.Serializable;
4 | import java.util.Date;
5 |
6 | import android.net.Uri;
7 | import io.kaif.mobile.model.Article;
8 | import io.kaif.mobile.model.Vote;
9 |
10 | public class ArticleViewModel implements Serializable {
11 |
12 | private Article article;
13 |
14 | private Vote vote;
15 |
16 | private Vote.VoteState prevVoteState;
17 |
18 | private Vote.VoteState currentVoteState;
19 |
20 | private boolean canShowVoteAnimation;
21 |
22 | public ArticleViewModel(Article article, Vote vote) {
23 | this.article = article;
24 | this.vote = vote;
25 | this.prevVoteState = vote.getVoteState();
26 | this.currentVoteState = vote.getVoteState();
27 | this.canShowVoteAnimation = true;
28 | }
29 |
30 | public String getZone() {
31 | return article.getZone();
32 | }
33 |
34 | public Date getCreateTime() {
35 | return article.getCreateTime();
36 | }
37 |
38 | public String getTitle() {
39 | return article.getTitle();
40 | }
41 |
42 | public Article.ArticleType getArticleType() {
43 | return article.getArticleType();
44 | }
45 |
46 | public long getScore() {
47 | return article.getUpVote() + currentVoteState.delta(vote.getVoteState());
48 | }
49 |
50 | public String getLink() {
51 | return article.getLink();
52 | }
53 |
54 | public long getDebateCount() {
55 | return article.getDebateCount();
56 | }
57 |
58 | public String getZoneTitle() {
59 | return article.getZoneTitle();
60 | }
61 |
62 | public String getArticleId() {
63 | return article.getArticleId();
64 | }
65 |
66 | public String getContent() {
67 | return article.getContent();
68 | }
69 |
70 | public String getAuthorName() {
71 | return article.getAuthorName();
72 | }
73 |
74 | public void setCanShowVoteAnimation(boolean canShowVoteAnimation) {
75 | this.canShowVoteAnimation = canShowVoteAnimation;
76 | }
77 |
78 | public boolean shouldShowVoteEffect() {
79 | if (!canShowVoteAnimation) {
80 | return false;
81 | }
82 |
83 | if (currentVoteState == prevVoteState && currentVoteState == Vote.VoteState.EMPTY) {
84 | return false;
85 | }
86 | return true;
87 | }
88 |
89 | public void updateVoteState(Vote.VoteState voteState) {
90 | prevVoteState = currentVoteState;
91 | currentVoteState = voteState;
92 | canShowVoteAnimation = true;
93 | }
94 |
95 | @Override
96 | public boolean equals(Object o) {
97 | if (this == o) {
98 | return true;
99 | }
100 | if (o == null || getClass() != o.getClass()) {
101 | return false;
102 | }
103 |
104 | ArticleViewModel that = (ArticleViewModel) o;
105 |
106 | return article.equals(that.article);
107 |
108 | }
109 |
110 | @Override
111 | public int hashCode() {
112 | return article.hashCode();
113 | }
114 |
115 | public Vote.VoteState getCurrentVoeState() {
116 | return currentVoteState;
117 | }
118 |
119 | public Uri getPermaLink() {
120 | return new Uri.Builder().scheme("https")
121 | .authority("kaif.io")
122 | .appendPath("z")
123 | .appendPath(getZone())
124 | .appendPath("debates")
125 | .appendPath(getArticleId())
126 | .build();
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/viewmodel/DebateViewModel.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.viewmodel;
2 |
3 | import java.util.Date;
4 |
5 | import io.kaif.mobile.model.Debate;
6 | import io.kaif.mobile.model.Vote;
7 |
8 | public class DebateViewModel {
9 |
10 | private final Debate debate;
11 |
12 | private final Vote vote;
13 |
14 | private Vote.VoteState prevVoteState;
15 |
16 | private Vote.VoteState currentVoteState;
17 |
18 | private boolean canShowVoteAnimation;
19 |
20 | public DebateViewModel(Debate debate, Vote vote) {
21 | this.debate = debate;
22 | this.vote = vote;
23 | this.prevVoteState = vote.getVoteState();
24 | this.currentVoteState = vote.getVoteState();
25 | canShowVoteAnimation = true;
26 | }
27 |
28 | public long getDownVote() {
29 | return debate.getDownVote();
30 | }
31 |
32 | public String getDebaterName() {
33 | return debate.getDebaterName();
34 | }
35 |
36 | public int getLevel() {
37 | return debate.getLevel();
38 | }
39 |
40 | public Date getLastUpdateTime() {
41 | return debate.getLastUpdateTime();
42 | }
43 |
44 | public String getDebateId() {
45 | return debate.getDebateId();
46 | }
47 |
48 | public String getParentDebateId() {
49 | return debate.getParentDebateId();
50 | }
51 |
52 | public String getZone() {
53 | return debate.getZone();
54 | }
55 |
56 | public long getUpVote() {
57 | return debate.getUpVote();
58 | }
59 |
60 | public String getContent() {
61 | return debate.getContent();
62 | }
63 |
64 | public String getArticleId() {
65 | return debate.getArticleId();
66 | }
67 |
68 | public Date getCreateTime() {
69 | return debate.getCreateTime();
70 | }
71 |
72 | public Vote.VoteState getCurrentVoeState() {
73 | return currentVoteState;
74 | }
75 |
76 | public long getVoteScore() {
77 | return debate.getUpVote() - debate.getDownVote()
78 | + (currentVoteState.delta(vote.getVoteState()));
79 | }
80 |
81 | public void setCanShowVoteAnimation(boolean canShowVoteAnimation) {
82 | this.canShowVoteAnimation = canShowVoteAnimation;
83 | }
84 |
85 | public boolean shouldShowVoteEffect() {
86 | if (!canShowVoteAnimation) {
87 | return false;
88 | }
89 | if (currentVoteState == prevVoteState) {
90 | return false;
91 | }
92 | return true;
93 | }
94 |
95 | public void updateVoteState(Vote.VoteState voteState) {
96 | prevVoteState = this.currentVoteState;
97 | currentVoteState = voteState;
98 | canShowVoteAnimation = true;
99 | }
100 |
101 | public Vote.VoteState getPrevVoteState() {
102 | return prevVoteState;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/viewmodel/FeedAssetViewModel.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.viewmodel;
2 |
3 | import io.kaif.mobile.model.FeedAsset;
4 | import io.kaif.mobile.model.Vote;
5 |
6 | public class FeedAssetViewModel {
7 |
8 | private DebateViewModel debateViewModel;
9 | private FeedAsset feedAsset;
10 | private boolean read;
11 |
12 | public FeedAssetViewModel(FeedAsset feedAsset, boolean read) {
13 | this.feedAsset = feedAsset;
14 | this.read = read;
15 | //doesn't support inline vote yet, provide fake vote state.
16 | this.debateViewModel = new DebateViewModel(feedAsset.getDebate(),
17 | Vote.abstain(feedAsset.getDebate().getDebateId()));
18 | }
19 |
20 | public DebateViewModel getDebateViewModel() {
21 | return debateViewModel;
22 | }
23 |
24 | public String getAssetId() {
25 | return feedAsset.getAssetId();
26 | }
27 |
28 | public boolean isRead() {
29 | return read;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/widget/ArticleScoreTextView.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.widget;
2 |
3 | import android.animation.Animator;
4 | import android.content.Context;
5 | import android.util.AttributeSet;
6 | import android.widget.TextView;
7 | import io.kaif.mobile.model.Vote;
8 | import io.kaif.mobile.view.animation.VoteAnimation;
9 |
10 | public class ArticleScoreTextView extends TextView {
11 |
12 | private Vote.VoteState voteState;
13 |
14 | public ArticleScoreTextView(Context context) {
15 | this(context, null);
16 | }
17 |
18 | public ArticleScoreTextView(Context context, AttributeSet attrs) {
19 | super(context, attrs);
20 | }
21 |
22 | public ArticleScoreTextView(Context context, AttributeSet attrs, int defStyleAttr) {
23 | super(context, attrs, defStyleAttr);
24 | }
25 |
26 | public void update(long score, Vote.VoteState voteState) {
27 | this.voteState = voteState;
28 | setText(String.valueOf(score));
29 | showVoteColor(false);
30 | }
31 |
32 | public void showVoteColor(boolean showAnimation) {
33 | final Animator animator;
34 | switch (voteState) {
35 | case UP: {
36 | animator = VoteAnimation.voteUpTextColorAnimation(this);
37 | break;
38 | }
39 | default:
40 | animator = VoteAnimation.voteUpReverseTextColorAnimation(this);
41 | break;
42 | }
43 | if (!showAnimation) {
44 | animator.setDuration(0);
45 | }
46 | animator.start();
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/widget/ClickableSpanTouchListener.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.widget;
2 |
3 | import android.text.Layout;
4 | import android.text.Spanned;
5 | import android.text.style.ClickableSpan;
6 | import android.view.MotionEvent;
7 | import android.view.View;
8 | import android.widget.TextView;
9 |
10 | /**
11 | * fix url span can't click problem
12 | */
13 | public class ClickableSpanTouchListener implements View.OnTouchListener {
14 |
15 | @Override
16 | public boolean onTouch(View v, MotionEvent event) {
17 | Spanned spanned = (Spanned) ((TextView) v).getText();
18 | TextView widget = (TextView) v;
19 | int action = event.getAction();
20 |
21 | if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
22 | int x = (int) event.getX();
23 | int y = (int) event.getY();
24 |
25 | x -= widget.getTotalPaddingLeft();
26 | y -= widget.getTotalPaddingTop();
27 |
28 | x += widget.getScrollX();
29 | y += widget.getScrollY();
30 |
31 | Layout layout = widget.getLayout();
32 | int line = layout.getLineForVertical(y);
33 | int off = layout.getOffsetForHorizontal(line, x);
34 |
35 | ClickableSpan[] link = spanned.getSpans(off, off, ClickableSpan.class);
36 |
37 | if (link.length != 0) {
38 | if (action == MotionEvent.ACTION_UP) {
39 | link[0].onClick(widget);
40 | }
41 | return true;
42 | }
43 | }
44 | return false;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/widget/OnScrollToLastListener.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.widget;
2 |
3 | import android.support.v7.widget.LinearLayoutManager;
4 | import android.support.v7.widget.RecyclerView;
5 |
6 | public abstract class OnScrollToLastListener extends RecyclerView.OnScrollListener {
7 |
8 | @Override
9 | public final void onScrolled(RecyclerView recyclerView, int dx, int dy) {
10 | LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
11 |
12 | int visibleItemCount = layoutManager.getChildCount();
13 | int totalItemCount = layoutManager.getItemCount();
14 | int pastVisibleItems = layoutManager.findFirstVisibleItemPosition();
15 |
16 | if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
17 | onScrollToLast();
18 | }
19 | }
20 |
21 | public abstract void onScrollToLast();
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/widget/OnVoteClickListener.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.widget;
2 |
3 | import io.kaif.mobile.model.Vote;
4 |
5 | public interface OnVoteClickListener {
6 | void onVoteClicked(Vote.VoteState from, Vote.VoteState to);
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/widget/ReplyDialog.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.widget;
2 |
3 | import android.app.Activity;
4 | import android.app.Dialog;
5 | import android.os.Bundle;
6 | import android.support.annotation.NonNull;
7 | import android.support.v7.app.AlertDialog;
8 | import android.view.KeyEvent;
9 | import android.view.LayoutInflater;
10 | import android.view.View;
11 | import android.view.inputmethod.EditorInfo;
12 | import android.widget.EditText;
13 | import android.widget.TextView;
14 | import android.widget.Toast;
15 |
16 | import com.trello.rxlifecycle.components.support.RxDialogFragment;
17 |
18 | import javax.inject.Inject;
19 |
20 | import butterknife.BindView;
21 | import butterknife.ButterKnife;
22 | import io.kaif.mobile.KaifApplication;
23 | import io.kaif.mobile.R;
24 | import io.kaif.mobile.view.daemon.DebateDaemon;
25 |
26 | public class ReplyDialog extends RxDialogFragment implements TextView.OnEditorActionListener {
27 |
28 | public static final int MIN_CONTENT_SIZE = 5;
29 |
30 | public static ReplyDialog createFragment(String article, String parentDebateId, int level) {
31 | ReplyDialog replyDialog = new ReplyDialog();
32 | Bundle args = new Bundle();
33 | args.putString("PARENT_DEBATE_ID", parentDebateId);
34 | args.putString("ARTICLE_ID", article);
35 | args.putInt("LEVEL", level);
36 | replyDialog.setArguments(args);
37 | return replyDialog;
38 | }
39 |
40 | private String getParentDebateId() {
41 | return getArguments().getString("PARENT_DEBATE_ID");
42 | }
43 |
44 | private String getArticleId() {
45 | return getArguments().getString("ARTICLE_ID");
46 | }
47 |
48 | private int getLevel() {
49 | return getArguments().getInt("LEVEL");
50 | }
51 |
52 | @Inject
53 | DebateDaemon debateDaemon;
54 |
55 | @BindView(R.id.debate_content)
56 | protected EditText contentEditText;
57 |
58 | public ReplyDialog() {
59 | // Empty constructor required for DialogFragment
60 | }
61 |
62 | @Override
63 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
64 | if (EditorInfo.IME_ACTION_DONE == actionId) {
65 | submitDebate();
66 | this.dismiss();
67 | return true;
68 | }
69 | return false;
70 | }
71 |
72 | @Override
73 | public void onAttach(Activity activity) {
74 | super.onAttach(activity);
75 | KaifApplication.getInstance().beans().inject(this);
76 | }
77 |
78 | @NonNull
79 | @Override
80 | public Dialog onCreateDialog(Bundle savedInstanceState) {
81 | LayoutInflater inflater = LayoutInflater.from(getActivity());
82 | View view = inflater.inflate(R.layout.fragment_reply, null);
83 | ButterKnife.bind(this, view);
84 | contentEditText.setOnEditorActionListener(this);
85 | return new AlertDialog.Builder(getActivity()).setTitle(R.string.reply)
86 | .setView(view)
87 | .setPositiveButton(R.string.submit_reply, (dialog, whichButton) -> {
88 | submitDebate();
89 | })
90 | .setNegativeButton(R.string.dialog_cancel, (dialog, whichButton) -> {
91 | })
92 | .create();
93 | }
94 |
95 | private void submitDebate() {
96 | String debateContent = contentEditText.getText().toString().trim();
97 | if (debateContent.length() < MIN_CONTENT_SIZE) {
98 | Toast.makeText(getActivity(), R.string.debate_too_short, Toast.LENGTH_SHORT).show();
99 | return;
100 | }
101 | debateDaemon.debate(getArticleId(), getParentDebateId(), getLevel(), debateContent);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/app/src/main/java/io/kaif/mobile/view/widget/VoteArticleButton.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view.widget;
2 |
3 | import android.animation.Animator;
4 | import android.content.Context;
5 | import android.graphics.drawable.InsetDrawable;
6 | import android.util.AttributeSet;
7 | import android.widget.Button;
8 | import io.kaif.mobile.R;
9 | import io.kaif.mobile.model.Vote;
10 | import io.kaif.mobile.view.animation.VoteAnimation;
11 | import io.kaif.mobile.view.graphics.drawable.Triangle;
12 | import io.kaif.mobile.view.util.Views;
13 |
14 | public class VoteArticleButton extends Button {
15 |
16 | private Vote.VoteState voteState;
17 |
18 | private OnVoteClickListener onVoteClickListener;
19 |
20 | public VoteArticleButton(Context context) {
21 | this(context, null);
22 | }
23 |
24 | public VoteArticleButton(Context context, AttributeSet attrs) {
25 | super(context, attrs);
26 | init(context);
27 | }
28 |
29 | public VoteArticleButton(Context context, AttributeSet attrs, int defStyleAttr) {
30 | super(context, attrs, defStyleAttr);
31 | init(context);
32 | }
33 |
34 | private void init(Context context) {
35 | setBackground(new InsetDrawable(new Triangle(context.getResources()
36 | .getColor(R.color.vote_state_empty)),
37 | (int) Views.convertDpToPixel(12, context),
38 | (int) Views.convertDpToPixel(4, context),
39 | (int) Views.convertDpToPixel(12, context),
40 | (int) Views.convertDpToPixel(4, context)));
41 | voteState = Vote.VoteState.EMPTY;
42 | setOnClickListener(v -> {
43 | if (onVoteClickListener != null) {
44 | Vote.VoteState from = this.voteState;
45 | Vote.VoteState to = (this.voteState == Vote.VoteState.EMPTY
46 | ? Vote.VoteState.UP
47 | : Vote.VoteState.EMPTY);
48 | onVoteClickListener.onVoteClicked(from, to);
49 | }
50 | });
51 | }
52 |
53 | public void setOnVoteClickListener(OnVoteClickListener onVoteClickListener) {
54 | this.onVoteClickListener = onVoteClickListener;
55 | }
56 |
57 | public void updateVoteState(Vote.VoteState voteState) {
58 | this.voteState = voteState;
59 | showVoteColor(false);
60 | }
61 |
62 | public void showVoteColor(boolean showAnimation) {
63 | final Animator animator;
64 | switch (voteState) {
65 | case UP: {
66 | animator = VoteAnimation.voteUpAnimation(this);
67 | break;
68 | }
69 | default:
70 | animator = VoteAnimation.voteUpReverseAnimation(this);
71 | break;
72 | }
73 | if (!showAnimation) {
74 | animator.setDuration(0);
75 | }
76 | animator.start();
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/io/kaif/mobile/view/HonorActivity.kt:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.support.design.widget.AppBarLayout
7 | import io.kaif.mobile.R
8 | import io.kaif.mobile.app.BaseActivity
9 | import org.jetbrains.anko.*
10 | import org.jetbrains.anko.appcompat.v7.themedToolbar
11 | import org.jetbrains.anko.design.coordinatorLayout
12 | import org.jetbrains.anko.design.themedAppBarLayout
13 |
14 | class HonorActivity : BaseActivity() {
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | HonorActivityUI().setContentView(this)
18 | setSupportActionBar(find(io.kaif.mobile.R.id.tool_bar))
19 | supportActionBar?.setDisplayHomeAsUpEnabled(true)
20 |
21 | if (savedInstanceState == null) {
22 | supportFragmentManager.beginTransaction()
23 | .replace(R.id.content_frame, HonorFragment.newInstance())
24 | .commit()
25 | }
26 | }
27 | }
28 |
29 | class HonorActivityIntent(context: Context) : Intent(context, HonorActivity::class.java)
30 |
31 | class HonorActivityUI : AnkoComponent {
32 | override fun createView(ui: AnkoContext) = with(ui) {
33 | coordinatorLayout {
34 | lparams(width = matchParent, height = matchParent)
35 | themedAppBarLayout {
36 | lparams(width = matchParent, height = wrapContent)
37 | themedToolbar(R.style.ThemeOverlay_AppCompat_Dark_ActionBar) {
38 | id = io.kaif.mobile.R.id.tool_bar
39 | popupTheme = R.style.ThemeOverlay_AppCompat_Light
40 | title = resources.getString(R.string.honor)
41 | }.lparams(width = matchParent, height = wrapContent) {
42 | scrollFlags = R.id.enterAlways
43 | }
44 | }
45 | frameLayout {
46 | id = io.kaif.mobile.R.id.content_frame
47 | }.lparams(width = matchParent, height = matchParent) {
48 | behavior = AppBarLayout.ScrollingViewBehavior()
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/io/kaif/mobile/view/HonorAdapter.kt:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view
2 |
3 | import android.support.v7.widget.RecyclerView
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.LinearLayout
7 | import android.widget.TextView
8 | import io.kaif.mobile.R
9 | import org.jetbrains.anko.*
10 |
11 | class HonorAdapter : RecyclerView.Adapter() {
12 |
13 | private val honors = arrayOf("123", "456", "789", "123", "456", "789", "123", "456", "789", "123", "456", "789", "123", "456", "789", "123", "456", "789")
14 |
15 | override fun getItemCount(): Int {
16 | return honors.size
17 | }
18 |
19 | override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): HonorViewHolder {
20 | return HonorViewHolder(HonorView().createView(AnkoContext.create(parent!!.context, parent)))
21 | }
22 |
23 | override fun onBindViewHolder(holder: HonorViewHolder?, position: Int) {
24 | holder!!.bind(honors[position])
25 | }
26 | }
27 |
28 | class HonorView : AnkoComponent {
29 | override fun createView(ui: AnkoContext): View {
30 | return with(ui) {
31 | linearLayout {
32 | lparams(width = matchParent, height = dip(48))
33 | orientation = LinearLayout.HORIZONTAL
34 | textView {
35 | id = R.id.title
36 | textSize = 16f
37 | }.lparams(width = matchParent, height = dip(48))
38 | }
39 | }
40 | }
41 | }
42 |
43 | class HonorViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
44 | var title: TextView? = itemView.findViewById(R.id.title)
45 |
46 | fun bind(data: String) {
47 | title?.text = "hi $data"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/io/kaif/mobile/view/HonorFragment.kt:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.view
2 |
3 | import android.os.Bundle
4 | import android.support.v7.widget.LinearLayoutManager
5 | import android.support.v7.widget.RecyclerView
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import io.kaif.mobile.KaifApplication
10 | import io.kaif.mobile.app.BaseFragment
11 | import org.jetbrains.anko.AnkoComponent
12 | import org.jetbrains.anko.AnkoContext
13 | import org.jetbrains.anko.frameLayout
14 | import org.jetbrains.anko.matchParent
15 | import org.jetbrains.anko.recyclerview.v7.recyclerView
16 | import org.jetbrains.anko.support.v4.ctx
17 |
18 | class HonorFragment : BaseFragment() {
19 |
20 | companion object {
21 | fun newInstance(): HonorFragment {
22 | return HonorFragment()
23 | }
24 | }
25 |
26 | lateinit var honors: RecyclerView
27 |
28 | private lateinit var honorsAdapter: HonorAdapter
29 |
30 | override fun onCreate(savedInstanceState: Bundle?) {
31 | super.onCreate(savedInstanceState)
32 | KaifApplication.getInstance().beans().inject(this)
33 | honorsAdapter = HonorAdapter()
34 | }
35 |
36 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
37 | return HonorFragmentUI(honorsAdapter).createView(AnkoContext.create(ctx, this))
38 | }
39 | }
40 |
41 |
42 | class HonorFragmentUI(private val honorAdapter: HonorAdapter) : AnkoComponent {
43 | override fun createView(ui: AnkoContext) = with(ui) {
44 | frameLayout {
45 | lparams(width = matchParent, height = matchParent)
46 | owner.honors = recyclerView {
47 | lparams(width = matchParent, height = matchParent)
48 | layoutManager = LinearLayoutManager(ctx)
49 | adapter = honorAdapter
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/rotate_vote.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/rotate_vote_back.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/scale_action_icon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_notifications_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-hdpi/ic_notifications_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_open_in_browser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-hdpi/ic_open_in_browser.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_record_voice_over_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-hdpi/ic_record_voice_over_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_reply.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-hdpi/ic_reply.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_send.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-hdpi/ic_send.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_notifications_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xhdpi/ic_notifications_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_open_in_browser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xhdpi/ic_open_in_browser.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_record_voice_over_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xhdpi/ic_record_voice_over_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_reply.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xhdpi/ic_reply.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_send.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xhdpi/ic_send.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_notifications_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xxhdpi/ic_notifications_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_open_in_browser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xxhdpi/ic_open_in_browser.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_record_voice_over_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xxhdpi/ic_record_voice_over_white.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_reply.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xxhdpi/ic_reply.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_send.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xxhdpi/ic_send.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/news_feed_divider.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/news_feed_divider_default.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/news_feed_divider_new.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/score_border.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_debates.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
17 |
18 |
23 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_home.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
17 |
18 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_login.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
13 |
14 |
21 |
22 |
28 |
29 |
30 |
31 |
41 |
42 |
43 |
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_news_feed.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
17 |
18 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_share_article.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_articles.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_debates.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
11 |
12 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_home.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
14 |
15 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_latest_debates.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
11 |
12 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_news_feed.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
11 |
12 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_reply.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_share_external_link.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
12 |
13 |
19 |
20 |
26 |
27 |
32 |
33 |
39 |
40 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_article.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
18 |
19 |
25 |
26 |
37 |
38 |
39 |
40 |
48 |
49 |
58 |
59 |
68 |
69 |
78 |
79 |
88 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_debate.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
11 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_debate_feed.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
17 |
18 |
23 |
24 |
25 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_debate_header.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
20 |
21 |
30 |
31 |
38 |
39 |
40 |
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_debate_latest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
11 |
12 |
15 |
16 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_loading.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
13 |
14 |
15 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_debates.xml:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_home.xml:
--------------------------------------------------------------------------------
1 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_share.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Kaif Android
4 | 設定
5 | 分享
6 | 登入
7 | 登出
8 | 尚未登入
9 | 帳號/密碼不合
10 | 密碼
11 | 標題
12 | 網址
13 | 帳戶名稱
14 | 討論區
15 | 登入成功
16 | 登出成功
17 | %1$s 則討論
18 | /z/%1$s
19 | network error
20 | 積分 %1$s
21 | 文章連結
22 | 個人想法
23 | 開啟瀏覽器
24 | 留言
25 | 送出
26 | 取消
27 | 留言過短
28 | 載入中…
29 | 登入中…
30 | 送出失敗
31 | 送出成功
32 | 該連結已經有人分享了
33 | 提醒
34 | 還是要分享
35 | 綜合熱門
36 | 綜合最新
37 | 最新討論
38 | 新消息
39 | 聲望
40 |
41 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #2D3E50
4 | #1b252f
5 | #d1dbe5
6 | #B3B3B3
7 | #FF6600
8 | #564FFF
9 | #777777
10 | #66cccccc
11 | #0078E7
12 | #ffcccccc
13 | #71BAEA
14 | #ff5619
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 |
5 | 16dp
6 | 16dp
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://kaif.io
4 | [full redirect uri]
5 | [host of redirect uri]
6 | [path of redirect uri]
7 | [scheme of redirect uri]
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Kaif Android
4 | Settings
5 | Share
6 | Sign in
7 | Sign out
8 | 尚未登入
9 | 帳號/密碼不合
10 | 密碼
11 | Title
12 | Url
13 | 帳戶名稱
14 | zone
15 | 登入成功
16 | 登出成功
17 | %1$s debates
18 | /z/%1$s
19 | network error
20 | Score %1$s
21 | External link
22 | IMO
23 | Open in browser
24 | Reply
25 | Submit
26 | Cancel
27 | Debate content too short
28 | Loading
29 | Processing…
30 | Submit failed
31 | Submit success
32 | The url already shared
33 | Warning
34 | Still share
35 | HOT
36 | NEW
37 | DEBATES
38 | News feed
39 | Honor
40 |
41 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/app/version.properties:
--------------------------------------------------------------------------------
1 | #Sun Jun 26 11:40:03 JST 2016
2 | VERSION_NAME_PREFIX=1.0
3 | VERSION_CODE=5
4 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | buildToolsVersion = "27.0.3"
4 | supportLibVersion = "27.0.2"
5 | runnerVersion = "1.0.1"
6 | rulesVersion = "1.0.1"
7 | espressoVersion = "3.0.1"
8 | daggerVersion = "2.14.1"
9 | butterknifeVersion = "8.8.1"
10 | retrofitVersion = "2.4.0"
11 | kotlinVersion = '1.2.30'
12 | ankoVersion = '0.10.4'
13 | }
14 | repositories {
15 | jcenter()
16 | google()
17 | }
18 | dependencies {
19 | classpath 'com.android.tools.build:gradle:3.1.2'
20 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
21 | }
22 | }
23 |
24 | allprojects {
25 | repositories {
26 | jcenter()
27 | google()
28 | maven {
29 | //for dagger2
30 | url "https://oss.sonatype.org/content/repositories/snapshots"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 | android.enableBuildCache=true
20 |
--------------------------------------------------------------------------------
/gradle/.gitignore:
--------------------------------------------------------------------------------
1 | /local.properties
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu May 03 21:12:16 JST 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/retry-stale-processor/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/retry-stale-processor/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java'
2 |
3 | dependencies {
4 | compile fileTree(dir: 'libs', include: ['*.jar'])
5 |
6 | compile 'com.google.guava:guava:18.0'
7 | compile 'com.google.auto.service:auto-service:1.0-rc2'
8 | compile 'com.squareup.retrofit2:retrofit:2.0.2'
9 | compile 'com.squareup:javapoet:1.0.0'
10 | compile 'io.reactivex:rxjava:1.1.6'
11 |
12 | testCompile 'com.google.testing.compile:compile-testing:0.6'
13 | testCompile 'junit:junit:4.12'
14 |
15 | }
--------------------------------------------------------------------------------
/retry-stale-processor/src/main/java/io/kaif/mobile/retrofit/processor/AnnotationSpecUtil.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.retrofit.processor;
2 |
3 | import java.util.List;
4 |
5 | import javax.lang.model.element.AnnotationMirror;
6 | import javax.lang.model.element.AnnotationValue;
7 | import javax.lang.model.element.ExecutableElement;
8 | import javax.lang.model.element.TypeElement;
9 | import javax.lang.model.element.VariableElement;
10 | import javax.lang.model.type.TypeMirror;
11 | import javax.lang.model.util.SimpleAnnotationValueVisitor7;
12 |
13 | import com.squareup.javapoet.AnnotationSpec;
14 | import com.squareup.javapoet.ClassName;
15 |
16 | /**
17 | * copy from https://github.com/square/javapoet/pull/268
18 | * delete this file after pull request merge into https://github.com/square/javapoet
19 | */
20 | public class AnnotationSpecUtil {
21 |
22 | public static AnnotationSpec generate(AnnotationMirror annotation) {
23 | TypeElement element = (TypeElement) annotation.getAnnotationType().asElement();
24 | AnnotationSpec.Builder builder = AnnotationSpec.builder(ClassName.get(element));
25 | Visitor visitor = new Visitor(builder);
26 | for (ExecutableElement executableElement : annotation.getElementValues().keySet()) {
27 | String name = executableElement.getSimpleName().toString();
28 | AnnotationValue value = annotation.getElementValues().get(executableElement);
29 | value.accept(visitor, new Entry(name, value));
30 | }
31 | return builder.build();
32 | }
33 |
34 | private static class Entry {
35 | final String name;
36 | final AnnotationValue value;
37 |
38 | Entry(String name, AnnotationValue value) {
39 | this.name = name;
40 | this.value = value;
41 | }
42 | }
43 |
44 | private static class Visitor
45 | extends SimpleAnnotationValueVisitor7 {
46 | final AnnotationSpec.Builder builder;
47 |
48 | Visitor(AnnotationSpec.Builder builder) {
49 | super(builder);
50 | this.builder = builder;
51 | }
52 |
53 | @Override
54 | public AnnotationSpec.Builder visitArray(List extends AnnotationValue> vals, Entry entry) {
55 | Visitor visitor = new Visitor(builder);
56 | vals.forEach(val -> val.accept(visitor, new Entry(entry.name, val)));
57 | return builder;
58 | }
59 |
60 | @Override
61 | protected AnnotationSpec.Builder defaultAction(Object o, Entry entry) {
62 | return builder.addMember(entry.name, "$L", entry.value);
63 | }
64 |
65 | @Override
66 | public AnnotationSpec.Builder visitAnnotation(AnnotationMirror a, Entry entry) {
67 | return builder.addMember(entry.name, "$L", generate(a));
68 | }
69 |
70 | @Override
71 | public AnnotationSpec.Builder visitEnumConstant(VariableElement c, Entry entry) {
72 | return builder.addMember(entry.name, "$T.$L", c.asType(), c.getSimpleName());
73 | }
74 |
75 | @Override
76 | public AnnotationSpec.Builder visitType(TypeMirror t, Entry entry) {
77 | return builder.addMember(entry.name, "$T.class", t);
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/retry-stale-processor/src/main/java/io/kaif/mobile/retrofit/processor/RetrofitServiceInterface.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.retrofit.processor;
2 |
3 | import javax.lang.model.element.Element;
4 | import javax.lang.model.element.ElementKind;
5 | import javax.lang.model.element.ExecutableElement;
6 | import javax.lang.model.element.Modifier;
7 | import javax.lang.model.element.TypeElement;
8 |
9 | import com.squareup.javapoet.TypeSpec;
10 |
11 | public class RetrofitServiceInterface {
12 |
13 | public static final String RETRY_STALE_POSTFIX = "$$RetryStale";
14 |
15 | public TypeElement getAnnotatedClassElement() {
16 | return annotatedClassElement;
17 | }
18 |
19 | private final TypeElement annotatedClassElement;
20 |
21 | public String getQualifiedName() {
22 | return annotatedClassElement.getQualifiedName() + RETRY_STALE_POSTFIX;
23 | }
24 |
25 | public RetrofitServiceInterface(TypeElement classElement) throws IllegalArgumentException {
26 | this.annotatedClassElement = classElement;
27 | }
28 |
29 | public TypeSpec createRetryStaleInterface() {
30 |
31 | TypeSpec.Builder typeSpecBuilder = TypeSpec.interfaceBuilder(annotatedClassElement.getSimpleName()
32 | + RETRY_STALE_POSTFIX).addModifiers(Modifier.PUBLIC);
33 |
34 | annotatedClassElement.getEnclosedElements()
35 | .stream()
36 | .filter(element -> element.getKind() == ElementKind.METHOD)
37 | .map(element -> new RetrofitServiceMethod((ExecutableElement) element))
38 | .flatMap(methodInfo -> methodInfo.generateCodeWithRetryStaleIfRequired().stream())
39 | .forEach(typeSpecBuilder::addMethod);
40 |
41 | return typeSpecBuilder.build();
42 | }
43 |
44 | public static boolean isGenerated(Element element) {
45 | return element.getSimpleName().toString().contains(RETRY_STALE_POSTFIX);
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/retry-stale-processor/src/main/java/io/kaif/mobile/retrofit/processor/RetrofitServiceMethod.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.retrofit.processor;
2 |
3 | import com.squareup.javapoet.AnnotationSpec;
4 | import com.squareup.javapoet.ClassName;
5 | import com.squareup.javapoet.CodeBlock;
6 | import com.squareup.javapoet.MethodSpec;
7 | import com.squareup.javapoet.ParameterSpec;
8 | import com.squareup.javapoet.TypeName;
9 |
10 | import java.util.List;
11 | import java.util.Optional;
12 |
13 | import javax.lang.model.element.ExecutableElement;
14 | import javax.lang.model.element.Modifier;
15 | import javax.lang.model.type.DeclaredType;
16 | import javax.lang.model.type.TypeKind;
17 |
18 | import retrofit2.http.GET;
19 | import retrofit2.http.Headers;
20 | import rx.Observable;
21 |
22 | import static java.util.Arrays.asList;
23 | import static java.util.Collections.singletonList;
24 | import static java.util.stream.Collectors.toList;
25 |
26 | public class RetrofitServiceMethod {
27 |
28 | private final ExecutableElement methodElement;
29 |
30 | private static final String CACHE_STALE_HEADER = "Cache-Control: max-stale=86400";
31 |
32 | public RetrofitServiceMethod(ExecutableElement methodElement) {
33 | this.methodElement = methodElement;
34 | }
35 |
36 | public List generateCodeWithRetryStaleIfRequired() {
37 |
38 | MethodSpec methodSpec = generateCode(false);
39 | if (!canAppendStaleHeader()) {
40 | return singletonList(methodSpec);
41 | }
42 | return asList(methodSpec, generateCode(true));
43 | }
44 |
45 | private MethodSpec generateCode(boolean withRetryStaleHeader) {
46 |
47 | MethodSpec.Builder builder = MethodSpec.methodBuilder(getMethodName(withRetryStaleHeader));
48 | builder.addModifiers(Modifier.ABSTRACT).addModifiers(Modifier.PUBLIC);
49 | methodElement.getParameters().stream().map(variableElement -> {
50 | ParameterSpec.Builder parameterBuilder = ParameterSpec.builder(TypeName.get(variableElement.asType()),
51 | variableElement.getSimpleName().toString());
52 | variableElement.getAnnotationMirrors()
53 | .stream()
54 | .map(AnnotationSpecUtil::generate)
55 | .forEach(parameterBuilder::addAnnotation);
56 | return parameterBuilder.build();
57 | }).forEach(builder::addParameter);
58 |
59 | List annotationSpecs = methodElement.getAnnotationMirrors()
60 | .stream()
61 | .map(AnnotationSpecUtil::generate)
62 | .collect(toList());
63 |
64 | if (withRetryStaleHeader) {
65 | Optional header = annotationSpecs.stream()
66 | .filter(annotationSpec -> isHeaderAnnotation(annotationSpec))
67 | .findAny();
68 | if (header.isPresent()) {
69 | annotationSpecs.forEach(annotationSpec -> {
70 | if (isHeaderAnnotation(annotationSpec)) {
71 | AnnotationSpec.Builder replace = AnnotationSpec.builder((ClassName) annotationSpec.type);
72 | annotationSpec.members.forEach((String s, List codeBlocks) -> {
73 | codeBlocks.forEach(codeBlock -> {
74 | replace.addMember(s, codeBlock);
75 | });
76 | });
77 | replace.addMember("value", "$S", CACHE_STALE_HEADER);
78 | builder.addAnnotation(replace.build());
79 | } else {
80 | builder.addAnnotation(annotationSpec);
81 | }
82 | });
83 | } else {
84 | annotationSpecs.forEach(builder::addAnnotation);
85 | builder.addAnnotation(AnnotationSpec.builder(Headers.class)
86 | .addMember("value", "$S", CACHE_STALE_HEADER)
87 | .build());
88 | }
89 | } else {
90 | annotationSpecs.forEach(builder::addAnnotation);
91 | }
92 |
93 | return builder.returns(TypeName.get(methodElement.getReturnType())).build();
94 | }
95 |
96 | private String getMethodName(boolean withRetryStaleHeader) {
97 | return methodElement.getSimpleName().toString() + (withRetryStaleHeader ? "$$RetryStale" : "");
98 | }
99 |
100 | private boolean canAppendStaleHeader() {
101 | if (methodElement.getAnnotation(GET.class) == null) {
102 | return false;
103 | }
104 | if (methodElement.getReturnType().getKind() != TypeKind.DECLARED) {
105 | return false;
106 | }
107 | String rawName = ((DeclaredType) methodElement.getReturnType()).asElement().toString();
108 | return rawName.equals(Observable.class.getCanonicalName());
109 |
110 | }
111 |
112 | private static boolean isHeaderAnnotation(AnnotationSpec annotationSpec) {
113 | return annotationSpec.type.toString().equals(Headers.class.getCanonicalName());
114 | }
115 |
116 | }
--------------------------------------------------------------------------------
/retry-stale-processor/src/main/java/io/kaif/mobile/retrofit/processor/RetrofitServiceProcessor.java:
--------------------------------------------------------------------------------
1 | package io.kaif.mobile.retrofit.processor;
2 |
3 | import com.google.auto.service.AutoService;
4 | import com.squareup.javapoet.JavaFile;
5 | import com.squareup.javapoet.TypeSpec;
6 |
7 | import java.io.IOException;
8 | import java.io.Writer;
9 | import java.util.LinkedHashSet;
10 | import java.util.Set;
11 |
12 | import javax.annotation.processing.AbstractProcessor;
13 | import javax.annotation.processing.Filer;
14 | import javax.annotation.processing.Messager;
15 | import javax.annotation.processing.ProcessingEnvironment;
16 | import javax.annotation.processing.Processor;
17 | import javax.annotation.processing.RoundEnvironment;
18 | import javax.lang.model.SourceVersion;
19 | import javax.lang.model.element.Element;
20 | import javax.lang.model.element.ElementKind;
21 | import javax.lang.model.element.TypeElement;
22 | import javax.lang.model.util.Elements;
23 | import javax.tools.Diagnostic;
24 | import javax.tools.JavaFileObject;
25 |
26 | import retrofit2.http.DELETE;
27 | import retrofit2.http.GET;
28 | import retrofit2.http.HEAD;
29 | import retrofit2.http.POST;
30 | import retrofit2.http.PUT;
31 |
32 | @AutoService(Processor.class)
33 | public class RetrofitServiceProcessor extends AbstractProcessor {
34 |
35 | private Elements elementUtils;
36 | private Filer filer;
37 | private Messager messager;
38 |
39 | @Override
40 | public synchronized void init(ProcessingEnvironment processingEnv) {
41 | super.init(processingEnv);
42 | elementUtils = processingEnv.getElementUtils();
43 | filer = processingEnv.getFiler();
44 | messager = processingEnv.getMessager();
45 | }
46 |
47 | @Override
48 | public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
49 | annotations.stream()
50 | .flatMap(typeElement -> roundEnv.getElementsAnnotatedWith(typeElement).stream())
51 | .filter(element -> element.getKind() == ElementKind.METHOD)
52 | .map(Element::getEnclosingElement)
53 | .distinct()
54 | .filter(element -> element.getKind() == ElementKind.INTERFACE)
55 | .filter(element -> !RetrofitServiceInterface.isGenerated(element))
56 | .map(element -> new RetrofitServiceInterface((TypeElement) element))
57 | .forEach(this::generateCode);
58 | return false;
59 | }
60 |
61 | private void generateCode(RetrofitServiceInterface retrofitServiceInterface) {
62 |
63 | TypeSpec typeSpec = retrofitServiceInterface.createRetryStaleInterface();
64 |
65 | TypeElement annotatedClassElement = retrofitServiceInterface.getAnnotatedClassElement();
66 | JavaFile javaFile = JavaFile.builder(elementUtils.getPackageOf(annotatedClassElement)
67 | .getQualifiedName()
68 | .toString(), typeSpec).build();
69 | try {
70 | JavaFileObject jfo = filer.createSourceFile(retrofitServiceInterface.getQualifiedName());
71 | try (Writer writer = jfo.openWriter()) {
72 | javaFile.writeTo(writer);
73 | }
74 | } catch (IOException e) {
75 | error(annotatedClassElement, e.getMessage());
76 | }
77 | }
78 |
79 | @Override
80 | public Set getSupportedAnnotationTypes() {
81 | Set annotations = new LinkedHashSet();
82 | annotations.add(GET.class.getCanonicalName());
83 | annotations.add(PUT.class.getCanonicalName());
84 | annotations.add(POST.class.getCanonicalName());
85 | annotations.add(DELETE.class.getCanonicalName());
86 | annotations.add(HEAD.class.getCanonicalName());
87 | return annotations;
88 | }
89 |
90 | private void error(Element e, String msg, Object... args) {
91 | messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e);
92 | }
93 |
94 | @Override
95 | public SourceVersion getSupportedSourceVersion() {
96 | return SourceVersion.latestSupported();
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':retry-stale-processor'
2 |
--------------------------------------------------------------------------------