@ -0,0 +1,261 @@ |
|||||
|
<html> |
||||
|
<head> |
||||
|
<style> |
||||
|
body { |
||||
|
font-family: sans-serif; |
||||
|
background-color: #333333; |
||||
|
} |
||||
|
|
||||
|
pre { |
||||
|
background-color: #3C3C3C; |
||||
|
color: #757575; |
||||
|
padding: 1em; |
||||
|
margin-left: 1em; |
||||
|
margin-right: 1em; |
||||
|
white-space: pre-wrap; |
||||
|
} |
||||
|
|
||||
|
h4, |
||||
|
h5 { |
||||
|
display: inline; |
||||
|
padding: 1em; |
||||
|
} |
||||
|
|
||||
|
a, |
||||
|
h4, |
||||
|
h5 { |
||||
|
color: #26A69A; |
||||
|
word-wrap: break-word; |
||||
|
} |
||||
|
|
||||
|
li { |
||||
|
color: #26A69A; |
||||
|
} |
||||
|
|
||||
|
</style> |
||||
|
</head> |
||||
|
|
||||
|
<body> |
||||
|
<ul> |
||||
|
<li> |
||||
|
<h5><a href="https://github.com/junit-team/junit4">JUnit</a> v4.12 (Copyright © 2002-2019, JUnit)</h5> |
||||
|
</li> |
||||
|
</ul> |
||||
|
|
||||
|
|
||||
|
<br/> |
||||
|
<h4>Eclipse Public License v1.0</h4> |
||||
|
<pre> |
||||
|
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC |
||||
|
LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM |
||||
|
CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. |
||||
|
|
||||
|
1. DEFINITIONS |
||||
|
|
||||
|
"Contribution" means: |
||||
|
|
||||
|
a) in the case of the initial Contributor, the initial code and |
||||
|
documentation distributed under this Agreement, and |
||||
|
b) in the case of each subsequent Contributor: |
||||
|
|
||||
|
i) changes to the Program, and |
||||
|
|
||||
|
ii) additions to the Program; |
||||
|
|
||||
|
where such changes and/or additions to the Program originate from and are |
||||
|
distributed by that particular Contributor. A Contribution 'originates' from a |
||||
|
Contributor if it was added to the Program by such Contributor itself or anyone |
||||
|
acting on such Contributor's behalf. Contributions do not include additions to |
||||
|
the Program which: (i) are separate modules of software distributed in |
||||
|
conjunction with the Program under their own license agreement, and (ii) are |
||||
|
not derivative works of the Program. |
||||
|
|
||||
|
"Contributor" means any person or entity that distributes the Program. |
||||
|
|
||||
|
"Licensed Patents " mean patent claims licensable by a Contributor which are |
||||
|
necessarily infringed by the use or sale of its Contribution alone or when |
||||
|
combined with the Program. |
||||
|
|
||||
|
"Program" means the Contributions distributed in accordance with this Agreement. |
||||
|
|
||||
|
"Recipient" means anyone who receives the Program under this Agreement, |
||||
|
including all Contributors. |
||||
|
|
||||
|
2. GRANT OF RIGHTS |
||||
|
|
||||
|
a) Subject to the terms of this Agreement, each Contributor hereby grants |
||||
|
Recipient a non-exclusive, worldwide, royalty-free copyright license to |
||||
|
reproduce, prepare derivative works of, publicly display, publicly perform, |
||||
|
distribute and sublicense the Contribution of such Contributor, if any, and |
||||
|
such derivative works, in source code and object code form. |
||||
|
|
||||
|
b) Subject to the terms of this Agreement, each Contributor hereby grants |
||||
|
Recipient a non-exclusive, worldwide, royalty-free patent license under |
||||
|
Licensed Patents to make, use, sell, offer to sell, import and otherwise |
||||
|
transfer the Contribution of such Contributor, if any, in source code and |
||||
|
object code form. This patent license shall apply to the combination of the |
||||
|
Contribution and the Program if, at the time the Contribution is added by the |
||||
|
Contributor, such addition of the Contribution causes such combination to be |
||||
|
covered by the Licensed Patents. The patent license shall not apply to any |
||||
|
other combinations which include the Contribution. No hardware per se is |
||||
|
licensed hereunder. |
||||
|
|
||||
|
c) Recipient understands that although each Contributor grants the |
||||
|
licenses to its Contributions set forth herein, no assurances are provided by |
||||
|
any Contributor that the Program does not infringe the patent or other |
||||
|
intellectual property rights of any other entity. Each Contributor disclaims |
||||
|
any liability to Recipient for claims brought by any other entity based on |
||||
|
infringement of intellectual property rights or otherwise. As a condition to |
||||
|
exercising the rights and licenses granted hereunder, each Recipient hereby |
||||
|
assumes sole responsibility to secure any other intellectual property rights |
||||
|
needed, if any. For example, if a third party patent license is required to |
||||
|
allow Recipient to distribute the Program, it is Recipient's responsibility to |
||||
|
acquire that license before distributing the Program. |
||||
|
|
||||
|
d) Each Contributor represents that to its knowledge it has sufficient |
||||
|
copyright rights in its Contribution, if any, to grant the copyright license |
||||
|
set forth in this Agreement. |
||||
|
|
||||
|
3. REQUIREMENTS |
||||
|
|
||||
|
A Contributor may choose to distribute the Program in object code form under |
||||
|
its own license agreement, provided that: |
||||
|
|
||||
|
a) it complies with the terms and conditions of this Agreement; and |
||||
|
|
||||
|
b) its license agreement: |
||||
|
|
||||
|
i) effectively disclaims on behalf of all Contributors all warranties and |
||||
|
conditions, express and implied, including warranties or conditions of title |
||||
|
and non-infringement, and implied warranties or conditions of merchantability |
||||
|
and fitness for a particular purpose; |
||||
|
|
||||
|
ii) effectively excludes on behalf of all Contributors all liability for |
||||
|
damages, including direct, indirect, special, incidental and consequential |
||||
|
damages, such as lost profits; |
||||
|
|
||||
|
iii) states that any provisions which differ from this Agreement are |
||||
|
offered by that Contributor alone and not by any other party; and |
||||
|
|
||||
|
iv) states that source code for the Program is available from such |
||||
|
Contributor, and informs licensees how to obtain it in a reasonable manner on |
||||
|
or through a medium customarily used for software exchange. |
||||
|
|
||||
|
When the Program is made available in source code form: |
||||
|
|
||||
|
a) it must be made available under this Agreement; and |
||||
|
|
||||
|
b) a copy of this Agreement must be included with each copy of the |
||||
|
Program. |
||||
|
|
||||
|
Contributors may not remove or alter any copyright notices contained within the |
||||
|
Program. |
||||
|
|
||||
|
Each Contributor must identify itself as the originator of its Contribution, if |
||||
|
any, in a manner that reasonably allows subsequent Recipients to identify the |
||||
|
originator of the Contribution. |
||||
|
|
||||
|
4. COMMERCIAL DISTRIBUTION |
||||
|
|
||||
|
Commercial distributors of software may accept certain responsibilities with |
||||
|
respect to end users, business partners and the like. While this license is |
||||
|
intended to facilitate the commercial use of the Program, the Contributor who |
||||
|
includes the Program in a commercial product offering should do so in a manner |
||||
|
which does not create potential liability for other Contributors. Therefore, if |
||||
|
a Contributor includes the Program in a commercial product offering, such |
||||
|
Contributor ("Commercial Contributor") hereby agrees to defend and indemnify |
||||
|
every other Contributor ("Indemnified Contributor") against any losses, damages |
||||
|
and costs (collectively "Losses") arising from claims, lawsuits and other legal |
||||
|
actions brought by a third party against the Indemnified Contributor to the |
||||
|
extent caused by the acts or omissions of such Commercial Contributor in |
||||
|
connection with its distribution of the Program in a commercial product |
||||
|
offering. The obligations in this section do not apply to any claims or Losses |
||||
|
relating to any actual or alleged intellectual property infringement. In order |
||||
|
to qualify, an Indemnified Contributor must: a) promptly notify the Commercial |
||||
|
Contributor in writing of such claim, and b) allow the Commercial Contributor |
||||
|
to control, and cooperate with the Commercial Contributor in, the defense and |
||||
|
any related settlement negotiations. The Indemnified Contributor may |
||||
|
participate in any such claim at its own expense. |
||||
|
|
||||
|
For example, a Contributor might include the Program in a commercial product |
||||
|
offering, Product X. That Contributor is then a Commercial Contributor. If that |
||||
|
Commercial Contributor then makes performance claims, or offers warranties |
||||
|
related to Product X, those performance claims and warranties are such |
||||
|
Commercial Contributor's responsibility alone. Under this section, the |
||||
|
Commercial Contributor would have to defend claims against the other |
||||
|
Contributors related to those performance claims and warranties, and if a court |
||||
|
requires any other Contributor to pay any damages as a result, the Commercial |
||||
|
Contributor must pay those damages. |
||||
|
|
||||
|
5. NO WARRANTY |
||||
|
|
||||
|
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN |
||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR |
||||
|
IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, |
||||
|
NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each |
||||
|
Recipient is solely responsible for determining the appropriateness of using |
||||
|
and distributing the Program and assumes all risks associated with its exercise |
||||
|
of rights under this Agreement, including but not limited to the risks and |
||||
|
costs of program errors, compliance with applicable laws, damage to or loss of |
||||
|
data, programs or equipment, and unavailability or interruption of operations. |
||||
|
|
||||
|
6. DISCLAIMER OF LIABILITY |
||||
|
|
||||
|
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY |
||||
|
CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST |
||||
|
PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||
|
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY |
||||
|
WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS |
||||
|
GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. |
||||
|
|
||||
|
7. GENERAL |
||||
|
|
||||
|
If any provision of this Agreement is invalid or unenforceable under applicable |
||||
|
law, it shall not affect the validity or enforceability of the remainder of the |
||||
|
terms of this Agreement, and without further action by the parties hereto, such |
||||
|
provision shall be reformed to the minimum extent necessary to make such |
||||
|
provision valid and enforceable. |
||||
|
|
||||
|
If Recipient institutes patent litigation against any |
||||
|
entity (including a cross-claim or counterclaim in a lawsuit) alleging that the |
||||
|
Program itself (excluding combinations of the Program with other software or |
||||
|
hardware) infringes such Recipient's patent(s), then such Recipient's rights |
||||
|
granted under Section 2(b) shall terminate as of the date such litigation is |
||||
|
filed. |
||||
|
|
||||
|
All Recipient's rights under this Agreement shall terminate if it fails to |
||||
|
comply with any of the material terms or conditions of this Agreement and does |
||||
|
not cure such failure in a reasonable period of time after becoming aware of |
||||
|
such noncompliance. If all Recipient's rights under this Agreement terminate, |
||||
|
Recipient agrees to cease use and distribution of the Program as soon as |
||||
|
reasonably practicable. However, Recipient's obligations under this Agreement |
||||
|
and any licenses granted by Recipient relating to the Program shall continue |
||||
|
and survive. |
||||
|
|
||||
|
Everyone is permitted to copy and distribute copies of this Agreement, but in |
||||
|
order to avoid inconsistency the Agreement is copyrighted and may only be |
||||
|
modified in the following manner. The Agreement Steward reserves the right to |
||||
|
publish new versions (including revisions) of this Agreement from time to time. |
||||
|
No one other than the Agreement Steward has the right to modify this Agreement. |
||||
|
The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to |
||||
|
serve as the Agreement Steward to a suitable separate entity. Each new version |
||||
|
of the Agreement will be given a distinguishing version number. The Program |
||||
|
(including Contributions) may always be distributed subject to the version of |
||||
|
the Agreement under which it was received. In addition, after a new version of |
||||
|
the Agreement is published, Contributor may elect to distribute the Program |
||||
|
(including its Contributions) under the new version. Except as expressly stated |
||||
|
in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to |
||||
|
the intellectual property of any Contributor under this Agreement, whether |
||||
|
expressly, by implication, estoppel or otherwise. All rights in the Program not |
||||
|
expressly granted under this Agreement are reserved. |
||||
|
|
||||
|
This Agreement is governed by the laws of the State of New York and the |
||||
|
intellectual property laws of the United States of America. No party to this |
||||
|
Agreement will bring a legal action under this Agreement more than one year |
||||
|
after the cause of action arose. Each party waives its rights to a jury trial |
||||
|
in any resulting litigation. |
||||
|
</pre> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
@ -1,158 +0,0 @@ |
|||||
package gr.thmmy.mthmmy.activities.bookmarks; |
|
||||
|
|
||||
import android.app.Activity; |
|
||||
import android.graphics.Typeface; |
|
||||
import android.graphics.drawable.Drawable; |
|
||||
import android.os.Build; |
|
||||
import android.os.Bundle; |
|
||||
import android.view.LayoutInflater; |
|
||||
import android.view.View; |
|
||||
import android.view.ViewGroup; |
|
||||
import android.widget.ImageButton; |
|
||||
import android.widget.LinearLayout; |
|
||||
import android.widget.TextView; |
|
||||
|
|
||||
import androidx.annotation.NonNull; |
|
||||
import androidx.fragment.app.Fragment; |
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; |
|
||||
|
|
||||
import java.util.ArrayList; |
|
||||
|
|
||||
import gr.thmmy.mthmmy.R; |
|
||||
import gr.thmmy.mthmmy.model.Bookmark; |
|
||||
|
|
||||
/** |
|
||||
* A {@link Fragment} subclass. |
|
||||
* Use the {@link BookmarksTopicFragment#newInstance} factory method to |
|
||||
* create an instance of this fragment. |
|
||||
*/ |
|
||||
public class BookmarksTopicFragment extends Fragment { |
|
||||
private static final String ARG_SECTION_NUMBER = "SECTION_NUMBER"; |
|
||||
private static final String ARG_TOPIC_BOOKMARKS = "TOPIC_BOOKMARKS"; |
|
||||
|
|
||||
static final String INTERACTION_CLICK_TOPIC_BOOKMARK = "CLICK_TOPIC_BOOKMARK"; |
|
||||
static final String INTERACTION_TOGGLE_TOPIC_NOTIFICATION = "TOGGLE_TOPIC_NOTIFICATION"; |
|
||||
static final String INTERACTION_REMOVE_TOPIC_BOOKMARK = "REMOVE_TOPIC_BOOKMARK"; |
|
||||
|
|
||||
ArrayList<Bookmark> topicBookmarks = null; |
|
||||
|
|
||||
private static Drawable notificationsEnabledButtonImage; |
|
||||
private static Drawable notificationsDisabledButtonImage; |
|
||||
|
|
||||
// Required empty public constructor
|
|
||||
public BookmarksTopicFragment() { |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Use ONLY this factory method to create a new instance of |
|
||||
* this fragment using the provided parameters. |
|
||||
* |
|
||||
* @return A new instance of fragment Forum. |
|
||||
*/ |
|
||||
public static BookmarksTopicFragment newInstance(int sectionNumber, String topicBookmarks) { |
|
||||
BookmarksTopicFragment fragment = new BookmarksTopicFragment(); |
|
||||
Bundle args = new Bundle(); |
|
||||
args.putInt(ARG_SECTION_NUMBER, sectionNumber); |
|
||||
args.putString(ARG_TOPIC_BOOKMARKS, topicBookmarks); |
|
||||
fragment.setArguments(args); |
|
||||
return fragment; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onCreate(Bundle savedInstanceState) { |
|
||||
super.onCreate(savedInstanceState); |
|
||||
if (getArguments() != null) { |
|
||||
String bundledTopicBookmarks = getArguments().getString(ARG_TOPIC_BOOKMARKS); |
|
||||
if (bundledTopicBookmarks != null) { |
|
||||
topicBookmarks = Bookmark.stringToArrayList(bundledTopicBookmarks); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) |
|
||||
notificationsEnabledButtonImage = getResources().getDrawable(R.drawable.ic_notification_on, null); |
|
||||
else |
|
||||
notificationsEnabledButtonImage = VectorDrawableCompat.create(getResources(), R.drawable.ic_notification_on, null); |
|
||||
|
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) |
|
||||
notificationsDisabledButtonImage = getResources().getDrawable(R.drawable.ic_notification_off, null); |
|
||||
else |
|
||||
notificationsDisabledButtonImage = VectorDrawableCompat.create(getResources(), R.drawable.ic_notification_off, null); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public View onCreateView(@NonNull LayoutInflater layoutInflater, ViewGroup container, |
|
||||
Bundle savedInstanceState) { |
|
||||
// Inflates the layout for this fragment
|
|
||||
final View rootView = layoutInflater.inflate(R.layout.fragment_bookmarks, container, false); |
|
||||
//bookmarks_topic_container
|
|
||||
final LinearLayout bookmarksLinearView = rootView.findViewById(R.id.bookmarks_container); |
|
||||
|
|
||||
if(this.topicBookmarks != null && !this.topicBookmarks.isEmpty()) { |
|
||||
for (final Bookmark bookmarkedTopic : topicBookmarks) { |
|
||||
if (bookmarkedTopic != null && bookmarkedTopic.getTitle() != null) { |
|
||||
final LinearLayout row = (LinearLayout) layoutInflater.inflate( |
|
||||
R.layout.fragment_bookmarks_row, bookmarksLinearView, false); |
|
||||
row.setOnClickListener(view -> { |
|
||||
Activity activity = getActivity(); |
|
||||
if (activity instanceof BookmarksActivity) { |
|
||||
((BookmarksActivity) activity).onTopicInteractionListener(INTERACTION_CLICK_TOPIC_BOOKMARK, bookmarkedTopic); |
|
||||
} |
|
||||
}); |
|
||||
((TextView) row.findViewById(R.id.bookmark_title)).setText(bookmarkedTopic.getTitle()); |
|
||||
|
|
||||
final ImageButton notificationsEnabledButton = row.findViewById(R.id.toggle_notification); |
|
||||
if (!bookmarkedTopic.isNotificationsEnabled()) { |
|
||||
notificationsEnabledButton.setImageDrawable(notificationsDisabledButtonImage); |
|
||||
} |
|
||||
|
|
||||
notificationsEnabledButton.setOnClickListener(view -> { |
|
||||
Activity activity = getActivity(); |
|
||||
if (activity instanceof BookmarksActivity) { |
|
||||
if (((BookmarksActivity) activity).onTopicInteractionListener(INTERACTION_TOGGLE_TOPIC_NOTIFICATION, bookmarkedTopic)) { |
|
||||
notificationsEnabledButton.setImageDrawable(notificationsEnabledButtonImage); |
|
||||
} else { |
|
||||
notificationsEnabledButton.setImageDrawable(notificationsDisabledButtonImage); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
(row.findViewById(R.id.remove_bookmark)).setOnClickListener(view -> { |
|
||||
Activity activity = getActivity(); |
|
||||
if (activity instanceof BookmarksActivity) { |
|
||||
((BookmarksActivity) activity).onTopicInteractionListener(INTERACTION_REMOVE_TOPIC_BOOKMARK, bookmarkedTopic); |
|
||||
topicBookmarks.remove(bookmarkedTopic); |
|
||||
} |
|
||||
row.setVisibility(View.GONE); |
|
||||
|
|
||||
if (topicBookmarks.isEmpty()){ |
|
||||
bookmarksLinearView.addView(bookmarksListEmptyMessage()); |
|
||||
} |
|
||||
}); |
|
||||
bookmarksLinearView.addView(row); |
|
||||
} |
|
||||
} |
|
||||
} else { |
|
||||
bookmarksLinearView.addView(bookmarksListEmptyMessage()); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
return rootView; |
|
||||
} |
|
||||
|
|
||||
private TextView bookmarksListEmptyMessage() { |
|
||||
TextView emptyBookmarksCategory = new TextView(this.getContext()); |
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( |
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); |
|
||||
params.setMargins(0, 12, 0, 0); |
|
||||
emptyBookmarksCategory.setLayoutParams(params); |
|
||||
emptyBookmarksCategory.setText(getString(R.string.empty_topic_bookmarks)); |
|
||||
emptyBookmarksCategory.setTypeface(emptyBookmarksCategory.getTypeface(), Typeface.BOLD); |
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
|
||||
emptyBookmarksCategory.setTextColor(this.getContext().getColor(R.color.primary_text)); |
|
||||
} else { |
|
||||
//noinspection deprecation
|
|
||||
emptyBookmarksCategory.setTextColor(this.getContext().getResources().getColor(R.color.primary_text)); |
|
||||
} |
|
||||
emptyBookmarksCategory.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); |
|
||||
return emptyBookmarksCategory; |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,73 @@ |
|||||
|
package gr.thmmy.mthmmy.utils; |
||||
|
|
||||
|
import androidx.annotation.VisibleForTesting; |
||||
|
|
||||
|
import static android.text.format.DateUtils.DAY_IN_MILLIS; |
||||
|
import static android.text.format.DateUtils.HOUR_IN_MILLIS; |
||||
|
import static android.text.format.DateUtils.MINUTE_IN_MILLIS; |
||||
|
import static android.text.format.DateUtils.SECOND_IN_MILLIS; |
||||
|
import static android.text.format.DateUtils.YEAR_IN_MILLIS; |
||||
|
|
||||
|
class DateTimeUtils { |
||||
|
|
||||
|
private static final long MONTH_IN_MILLIS = 30*DAY_IN_MILLIS; |
||||
|
private static final long DECADE_IN_MILLIS = 10*YEAR_IN_MILLIS; |
||||
|
|
||||
|
@VisibleForTesting |
||||
|
static String getRelativeTimeSpanString(long time) { |
||||
|
long now = System.currentTimeMillis(); |
||||
|
|
||||
|
boolean past = (now >= time); |
||||
|
long duration = Math.abs(now - time); |
||||
|
String format; |
||||
|
long count, mod; |
||||
|
if(duration < 45*SECOND_IN_MILLIS) |
||||
|
return "just now"; |
||||
|
else if (duration < 45*MINUTE_IN_MILLIS) { |
||||
|
count = duration/MINUTE_IN_MILLIS; |
||||
|
mod = duration % MINUTE_IN_MILLIS; |
||||
|
if(mod >= 30*SECOND_IN_MILLIS) |
||||
|
count += 1; |
||||
|
format = "%dm"; |
||||
|
} else if (duration < 22*HOUR_IN_MILLIS) { |
||||
|
count = duration/HOUR_IN_MILLIS; |
||||
|
format = "%dh"; |
||||
|
mod = (duration%HOUR_IN_MILLIS)/MINUTE_IN_MILLIS; |
||||
|
if(count<3 && mod>9 && mod<51){ |
||||
|
if(count==0) |
||||
|
format = mod +"m"; |
||||
|
else |
||||
|
format = format + " " + mod +"m"; |
||||
|
} |
||||
|
else if(mod >= 30) |
||||
|
count += 1; |
||||
|
} else if (duration < 26*DAY_IN_MILLIS) { |
||||
|
count = duration/DAY_IN_MILLIS; |
||||
|
format = "%dd"; |
||||
|
mod = duration % DAY_IN_MILLIS; |
||||
|
if(mod >= 12*HOUR_IN_MILLIS) |
||||
|
count += 1; |
||||
|
} else if (duration < 320*DAY_IN_MILLIS) { |
||||
|
count = duration/MONTH_IN_MILLIS; |
||||
|
format = "%d month"; |
||||
|
mod = duration % MONTH_IN_MILLIS; |
||||
|
if(mod >= 15*DAY_IN_MILLIS) |
||||
|
count += 1; |
||||
|
if(count>1) |
||||
|
format = format + 's'; |
||||
|
} else if (duration < DECADE_IN_MILLIS) { |
||||
|
count = duration/YEAR_IN_MILLIS; |
||||
|
format = "%d year"; |
||||
|
mod = duration % YEAR_IN_MILLIS; |
||||
|
if(mod >= 183*DAY_IN_MILLIS) |
||||
|
count += 1; |
||||
|
if(count>1) |
||||
|
format = format + 's'; |
||||
|
} |
||||
|
else |
||||
|
return past ? "a long time ago": "in the distant future"; |
||||
|
|
||||
|
format = past ? format : "in " + format; |
||||
|
return String.format(format, (int) count); |
||||
|
} |
||||
|
} |
@ -0,0 +1,232 @@ |
|||||
|
package gr.thmmy.mthmmy.utils; |
||||
|
|
||||
|
import android.annotation.SuppressLint; |
||||
|
import android.content.Context; |
||||
|
import android.content.res.TypedArray; |
||||
|
import android.os.Handler; |
||||
|
import android.os.Parcel; |
||||
|
import android.os.Parcelable; |
||||
|
import android.text.format.DateUtils; |
||||
|
import android.util.AttributeSet; |
||||
|
import android.view.View; |
||||
|
import android.widget.TextView; |
||||
|
|
||||
|
import java.lang.ref.WeakReference; |
||||
|
|
||||
|
import gr.thmmy.mthmmy.R; |
||||
|
|
||||
|
import static gr.thmmy.mthmmy.utils.DateTimeUtils.getRelativeTimeSpanString; |
||||
|
|
||||
|
/** |
||||
|
* A modified version of https://github.com/curioustechizen/android-ago
|
||||
|
*/ |
||||
|
@SuppressLint("AppCompatCustomView") |
||||
|
public class RelativeTimeTextView extends TextView { |
||||
|
|
||||
|
private static final long INITIAL_UPDATE_INTERVAL = DateUtils.MINUTE_IN_MILLIS; |
||||
|
|
||||
|
private long mReferenceTime; |
||||
|
private Handler mHandler = new Handler(); |
||||
|
private UpdateTimeRunnable mUpdateTimeTask; |
||||
|
private boolean isUpdateTaskRunning = false; |
||||
|
|
||||
|
public RelativeTimeTextView(Context context, AttributeSet attrs) { |
||||
|
super(context, attrs); |
||||
|
init(context, attrs); |
||||
|
} |
||||
|
|
||||
|
public RelativeTimeTextView(Context context, AttributeSet attrs, int defStyle) { |
||||
|
super(context, attrs, defStyle); |
||||
|
init(context, attrs); |
||||
|
} |
||||
|
|
||||
|
private void init(Context context, AttributeSet attrs) { |
||||
|
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, |
||||
|
R.styleable.RelativeTimeTextView, 0, 0); |
||||
|
String referenceTimeText; |
||||
|
try { |
||||
|
referenceTimeText = a.getString(R.styleable.RelativeTimeTextView_reference_time); |
||||
|
} finally { |
||||
|
a.recycle(); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
mReferenceTime = Long.valueOf(referenceTimeText); |
||||
|
} catch (NumberFormatException nfe) { |
||||
|
/* |
||||
|
* TODO: Better exception handling |
||||
|
*/ |
||||
|
mReferenceTime = -1L; |
||||
|
} |
||||
|
|
||||
|
setReferenceTime(mReferenceTime); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Sets the reference time for this view. At any moment, the view will render a relative time period relative to the time set here. |
||||
|
* <p/> |
||||
|
* This value can also be set with the XML attribute {@code reference_time} |
||||
|
* @param referenceTime The timestamp (in milliseconds since epoch) that will be the reference point for this view. |
||||
|
*/ |
||||
|
public void setReferenceTime(long referenceTime) { |
||||
|
this.mReferenceTime = referenceTime; |
||||
|
|
||||
|
/* |
||||
|
* Note that this method could be called when a row in a ListView is recycled. |
||||
|
* Hence, we need to first stop any currently running schedules (for example from the recycled view. |
||||
|
*/ |
||||
|
stopTaskForPeriodicallyUpdatingRelativeTime(); |
||||
|
|
||||
|
/* |
||||
|
* Instantiate a new runnable with the new reference time |
||||
|
*/ |
||||
|
initUpdateTimeTask(); |
||||
|
|
||||
|
/* |
||||
|
* Start a new schedule. |
||||
|
*/ |
||||
|
startTaskForPeriodicallyUpdatingRelativeTime(); |
||||
|
|
||||
|
/* |
||||
|
* Finally, update the text display. |
||||
|
*/ |
||||
|
updateTextDisplay(); |
||||
|
} |
||||
|
|
||||
|
private void updateTextDisplay() { |
||||
|
/* |
||||
|
* TODO: Validation, Better handling of negative cases |
||||
|
*/ |
||||
|
if (this.mReferenceTime == -1L) |
||||
|
return; |
||||
|
setText(getRelativeTimeSpanString(mReferenceTime)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void onAttachedToWindow() { |
||||
|
super.onAttachedToWindow(); |
||||
|
startTaskForPeriodicallyUpdatingRelativeTime(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void onDetachedFromWindow() { |
||||
|
super.onDetachedFromWindow(); |
||||
|
stopTaskForPeriodicallyUpdatingRelativeTime(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void onVisibilityChanged(View changedView, int visibility) { |
||||
|
super.onVisibilityChanged(changedView, visibility); |
||||
|
if (visibility == GONE || visibility == INVISIBLE) { |
||||
|
stopTaskForPeriodicallyUpdatingRelativeTime(); |
||||
|
} else { |
||||
|
startTaskForPeriodicallyUpdatingRelativeTime(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void startTaskForPeriodicallyUpdatingRelativeTime() { |
||||
|
if(mUpdateTimeTask.isDetached()) initUpdateTimeTask(); |
||||
|
mHandler.post(mUpdateTimeTask); |
||||
|
isUpdateTaskRunning = true; |
||||
|
} |
||||
|
|
||||
|
private void initUpdateTimeTask() { |
||||
|
mUpdateTimeTask = new UpdateTimeRunnable(this, mReferenceTime); |
||||
|
} |
||||
|
|
||||
|
private void stopTaskForPeriodicallyUpdatingRelativeTime() { |
||||
|
if(isUpdateTaskRunning) { |
||||
|
mUpdateTimeTask.detach(); |
||||
|
mHandler.removeCallbacks(mUpdateTimeTask); |
||||
|
isUpdateTaskRunning = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Parcelable onSaveInstanceState() { |
||||
|
Parcelable superState = super.onSaveInstanceState(); |
||||
|
SavedState ss = new SavedState(superState); |
||||
|
ss.referenceTime = mReferenceTime; |
||||
|
return ss; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onRestoreInstanceState(Parcelable state) { |
||||
|
if (!(state instanceof SavedState)) { |
||||
|
super.onRestoreInstanceState(state); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
SavedState ss = (SavedState)state; |
||||
|
mReferenceTime = ss.referenceTime; |
||||
|
super.onRestoreInstanceState(ss.getSuperState()); |
||||
|
} |
||||
|
|
||||
|
public static class SavedState extends BaseSavedState { |
||||
|
|
||||
|
private long referenceTime; |
||||
|
|
||||
|
public SavedState(Parcelable superState) { |
||||
|
super(superState); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void writeToParcel(Parcel dest, int flags) { |
||||
|
super.writeToParcel(dest, flags); |
||||
|
dest.writeLong(referenceTime); |
||||
|
} |
||||
|
|
||||
|
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { |
||||
|
public SavedState createFromParcel(Parcel in) { |
||||
|
return new SavedState(in); |
||||
|
} |
||||
|
|
||||
|
public SavedState[] newArray(int size) { |
||||
|
return new SavedState[size]; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
private SavedState(Parcel in) { |
||||
|
super(in); |
||||
|
referenceTime = in.readLong(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static class UpdateTimeRunnable implements Runnable { |
||||
|
|
||||
|
private long mRefTime; |
||||
|
private final WeakReference<RelativeTimeTextView> weakRefRttv; |
||||
|
|
||||
|
UpdateTimeRunnable(RelativeTimeTextView rttv, long refTime) { |
||||
|
this.mRefTime = refTime; |
||||
|
weakRefRttv = new WeakReference<>(rttv); |
||||
|
} |
||||
|
|
||||
|
boolean isDetached() { |
||||
|
return weakRefRttv.get() == null; |
||||
|
} |
||||
|
|
||||
|
void detach() { |
||||
|
weakRefRttv.clear(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void run() { |
||||
|
RelativeTimeTextView rttv = weakRefRttv.get(); |
||||
|
if (rttv == null) return; |
||||
|
long difference = Math.abs(System.currentTimeMillis() - mRefTime); |
||||
|
long interval = INITIAL_UPDATE_INTERVAL; |
||||
|
if (difference > DateUtils.WEEK_IN_MILLIS) { |
||||
|
interval = DateUtils.WEEK_IN_MILLIS; |
||||
|
} else if (difference > DateUtils.DAY_IN_MILLIS) { |
||||
|
interval = DateUtils.DAY_IN_MILLIS; |
||||
|
} else if (difference > DateUtils.HOUR_IN_MILLIS) { |
||||
|
interval = DateUtils.HOUR_IN_MILLIS; |
||||
|
} |
||||
|
rttv.updateTextDisplay(); |
||||
|
rttv.mHandler.postDelayed(this, interval); |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,106 @@ |
|||||
|
package gr.thmmy.mthmmy.utils.parsing; |
||||
|
|
||||
|
import androidx.annotation.VisibleForTesting; |
||||
|
|
||||
|
import org.joda.time.DateTime; |
||||
|
import org.joda.time.DateTimeUtils; |
||||
|
import org.joda.time.DateTimeZone; |
||||
|
import org.joda.time.format.DateTimeFormat; |
||||
|
import org.joda.time.format.DateTimeFormatter; |
||||
|
import org.joda.time.format.DateTimeFormatterBuilder; |
||||
|
import org.joda.time.format.DateTimeParser; |
||||
|
|
||||
|
import java.text.DateFormatSymbols; |
||||
|
import java.util.Locale; |
||||
|
import java.util.regex.Matcher; |
||||
|
import java.util.regex.Pattern; |
||||
|
|
||||
|
import gr.thmmy.mthmmy.base.BaseApplication; |
||||
|
import timber.log.Timber; |
||||
|
|
||||
|
public class ThmmyDateTimeParser { |
||||
|
private static final DateTimeParser[] parsers = { |
||||
|
DateTimeFormat.forPattern("hh:mm:ss a").getParser(), |
||||
|
DateTimeFormat.forPattern("HH:mm:ss").getParser(), |
||||
|
DateTimeFormat.forPattern("MMMM d, Y, hh:mm:ss a").getParser(), |
||||
|
DateTimeFormat.forPattern("MMMM d, Y, HH:mm:ss").getParser(), |
||||
|
DateTimeFormat.forPattern("d MMMM Y, HH:mm:ss").getParser(), |
||||
|
DateTimeFormat.forPattern("Y-M-d, HH:mm:ss").getParser(), |
||||
|
DateTimeFormat.forPattern("d-M-Y, HH:mm:ss").getParser() |
||||
|
}; |
||||
|
|
||||
|
private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder() |
||||
|
.append(null, parsers) |
||||
|
.toFormatter(); |
||||
|
|
||||
|
//TODO: Replace with Locale.forLanguageTag() (with "el-GR","en-US") when KitKat support is dropped
|
||||
|
private static final Locale greekLocale = new Locale("el", "GR"); |
||||
|
private static final Locale englishLocale = new Locale("en", "US"); |
||||
|
|
||||
|
private static final Pattern pattern = Pattern.compile("\\s((1[3-9]|2[0-3]):)"); |
||||
|
|
||||
|
private ThmmyDateTimeParser(){} |
||||
|
|
||||
|
public static String convertToTimestamp(String thmmyDateTime){ |
||||
|
Timber.d("Will attempt to convert %s to timestamp.", thmmyDateTime); |
||||
|
String originalDateTime = thmmyDateTime; |
||||
|
DateTimeZone dtz = getDtz(); |
||||
|
|
||||
|
//Add today's date for the first two cases
|
||||
|
if(thmmyDateTime.charAt(2)==':') |
||||
|
thmmyDateTime = (new DateTime()).toString("MMMM d, Y, ") + thmmyDateTime; |
||||
|
|
||||
|
//Don't even ask
|
||||
|
if(thmmyDateTime.contains("am")) |
||||
|
thmmyDateTime = thmmyDateTime.replaceAll("\\s00:"," 12:"); |
||||
|
|
||||
|
// For the stupid format 23:54:12 pm
|
||||
|
Matcher matcher = pattern.matcher(thmmyDateTime); |
||||
|
if (matcher.find()) |
||||
|
thmmyDateTime = thmmyDateTime.replaceAll("\\spm",""); |
||||
|
|
||||
|
DateTime dateTime; |
||||
|
try{ |
||||
|
dateTime=formatter.withZone(dtz).withLocale(englishLocale).parseDateTime(thmmyDateTime); |
||||
|
} |
||||
|
catch (IllegalArgumentException e1){ |
||||
|
Timber.d("Parsing DateTime %s using English Locale failed.", thmmyDateTime); |
||||
|
try{ |
||||
|
DateFormatSymbols dfs = DateTimeUtils.getDateFormatSymbols(greekLocale); |
||||
|
thmmyDateTime = thmmyDateTime.replace("am",dfs.getAmPmStrings()[0]); |
||||
|
thmmyDateTime = thmmyDateTime.replace("pm",dfs.getAmPmStrings()[1]); |
||||
|
Timber.d("Attempting to parse DateTime %s using Greek Locale...", thmmyDateTime); |
||||
|
dateTime=formatter.withZone(dtz).withLocale(greekLocale).parseDateTime(thmmyDateTime); |
||||
|
} |
||||
|
catch (IllegalArgumentException e2){ |
||||
|
Timber.d("Parsing DateTime %s using Greek Locale failed too.", thmmyDateTime); |
||||
|
Timber.e("Couldn't convert DateTime %s to timestamp!", originalDateTime); |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
String timestamp = Long.toString(dateTime.getMillis()); |
||||
|
Timber.d("DateTime %s was converted to %s, or %s", originalDateTime, timestamp, dateTime.toString()); |
||||
|
|
||||
|
return timestamp; |
||||
|
} |
||||
|
|
||||
|
public static String convertDateTime(String dateTime, boolean removeSeconds){ |
||||
|
//Convert e.g. Today at 12:16:48 -> 12:16:48, but October 03, 2019, 16:40:18 remains as is
|
||||
|
if (!dateTime.contains(",")) |
||||
|
dateTime = dateTime.replaceAll(".+? ([0-9])", "$1"); |
||||
|
|
||||
|
//Remove seconds
|
||||
|
if(removeSeconds) |
||||
|
dateTime = dateTime.replaceAll("(.+?)(:[0-5][0-9])($|\\s)", "$1$3"); |
||||
|
|
||||
|
return dateTime; |
||||
|
} |
||||
|
|
||||
|
@VisibleForTesting |
||||
|
private static DateTimeZone getDtz(){ |
||||
|
if(!BaseApplication.getInstance().getSessionManager().isLoggedIn()) |
||||
|
return DateTimeZone.forID("Europe/Athens"); |
||||
|
else |
||||
|
return DateTimeZone.getDefault(); |
||||
|
} |
||||
|
} |
@ -0,0 +1,106 @@ |
|||||
|
package gr.thmmy.mthmmy.utils; |
||||
|
|
||||
|
import net.lachlanmckee.timberjunit.TimberTestRule; |
||||
|
|
||||
|
import org.joda.time.DateTime; |
||||
|
import org.junit.Rule; |
||||
|
import org.junit.Test; |
||||
|
import org.junit.runner.RunWith; |
||||
|
import org.powermock.api.mockito.PowerMockito; |
||||
|
import org.powermock.core.classloader.annotations.PrepareForTest; |
||||
|
import org.powermock.modules.junit4.PowerMockRunner; |
||||
|
|
||||
|
import static gr.thmmy.mthmmy.utils.DateTimeUtils.getRelativeTimeSpanString; |
||||
|
import static org.junit.Assert.assertArrayEquals; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
@RunWith(PowerMockRunner.class) |
||||
|
@PrepareForTest(DateTimeUtils.class) |
||||
|
public class DateTimeUtilsTest { |
||||
|
@Rule |
||||
|
public TimberTestRule logAllAlwaysRule = TimberTestRule.logAllAlways(); |
||||
|
|
||||
|
private final long NOW = System.currentTimeMillis(); |
||||
|
private final String [] expectedRelativeTimeSpans = { |
||||
|
"just now", |
||||
|
"just now", |
||||
|
"just now", |
||||
|
"1m", |
||||
|
"1m", |
||||
|
"1m", |
||||
|
"2m", |
||||
|
"3m", |
||||
|
"1h", |
||||
|
"1h 15m", |
||||
|
"2h", |
||||
|
"2h 20m", |
||||
|
"4h", |
||||
|
"20h", |
||||
|
"21h", |
||||
|
"21h", |
||||
|
"21h", |
||||
|
"22h", |
||||
|
"1d", |
||||
|
"1d", |
||||
|
"2d", |
||||
|
"2d", |
||||
|
"3d", |
||||
|
"16d", |
||||
|
"1 month", |
||||
|
"2 months", |
||||
|
"1 year", |
||||
|
"1 year", |
||||
|
"2 years", |
||||
|
"a long time ago" |
||||
|
}; |
||||
|
|
||||
|
private final long [] times = { |
||||
|
NOW, |
||||
|
newDT().minusSeconds(44).getMillis(), |
||||
|
newDT().minusSeconds(44).minusMillis(500).getMillis(), |
||||
|
newDT().minusSeconds(45).getMillis(), |
||||
|
newDT().minusSeconds(89).getMillis(), |
||||
|
newDT().minusSeconds(89).minusMillis(500).getMillis(), |
||||
|
newDT().minusSeconds(90).getMillis(), |
||||
|
newDT().minusMinutes(3).minusSeconds(10).getMillis(), |
||||
|
newDT().minusHours(1).minusMinutes(4).getMillis(), |
||||
|
newDT().minusHours(1).minusMinutes(15).getMillis(), |
||||
|
newDT().minusHours(2).minusMinutes(4).getMillis(), |
||||
|
newDT().minusHours(2).minusMinutes(20).getMillis(), |
||||
|
newDT().minusHours(3).minusMinutes(51).getMillis(), |
||||
|
newDT().minusHours(20).minusMinutes(10).getMillis(), |
||||
|
newDT().minusHours(20).minusMinutes(30).getMillis(), |
||||
|
newDT().minusHours(21).getMillis(), |
||||
|
newDT().minusHours(21).minusMinutes(29).getMillis(), |
||||
|
newDT().minusHours(21).minusMinutes(30).getMillis(), |
||||
|
newDT().minusHours(22).minusMinutes(30).getMillis(), |
||||
|
newDT().minusHours(34).getMillis(), |
||||
|
newDT().minusHours(38).getMillis(), |
||||
|
newDT().minusDays(2).minusHours(10).getMillis(), |
||||
|
newDT().minusDays(2).minusHours(17).getMillis(), |
||||
|
newDT().minusDays(16).getMillis(), |
||||
|
newDT().minusDays(30+12).getMillis(), |
||||
|
newDT().minusDays(2*30+14).getMillis(), |
||||
|
newDT().minusDays(14*30).getMillis(), |
||||
|
newDT().minusMonths(15).getMillis(), |
||||
|
newDT().minusMonths(22).getMillis(), |
||||
|
newDT().minusYears(22).getMillis() |
||||
|
}; |
||||
|
|
||||
|
private DateTime newDT(){ |
||||
|
return new DateTime(NOW); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void relativeTimeSpansAreConvertedCorrectly() { |
||||
|
PowerMockito.mockStatic(System.class); |
||||
|
when(System.currentTimeMillis()).thenReturn(NOW); |
||||
|
|
||||
|
String[] timeStrings = new String[times.length]; |
||||
|
|
||||
|
for(int i=0; i<times.length; i++) |
||||
|
timeStrings[i] = getRelativeTimeSpanString(times[i]); |
||||
|
|
||||
|
assertArrayEquals(expectedRelativeTimeSpans,timeStrings); |
||||
|
} |
||||
|
} |
@ -0,0 +1,85 @@ |
|||||
|
package gr.thmmy.mthmmy.utils.parsing; |
||||
|
|
||||
|
import net.lachlanmckee.timberjunit.TimberTestRule; |
||||
|
|
||||
|
import org.joda.time.DateTimeZone; |
||||
|
import org.junit.Rule; |
||||
|
import org.junit.Test; |
||||
|
import org.junit.runner.RunWith; |
||||
|
import org.powermock.core.classloader.annotations.PrepareForTest; |
||||
|
import org.powermock.modules.junit4.PowerMockRunner; |
||||
|
|
||||
|
import static gr.thmmy.mthmmy.utils.parsing.ThmmyDateTimeParser.convertToTimestamp; |
||||
|
import static org.junit.Assert.assertArrayEquals; |
||||
|
import static org.junit.Assert.assertNotNull; |
||||
|
import static org.powermock.api.support.membermodification.MemberMatcher.method; |
||||
|
import static org.powermock.api.support.membermodification.MemberModifier.stub; |
||||
|
|
||||
|
@RunWith(PowerMockRunner.class) |
||||
|
@PrepareForTest(ThmmyDateTimeParser.class) |
||||
|
public class ThmmyDateTimeParserTest { |
||||
|
@Rule |
||||
|
public TimberTestRule logAllAlwaysRule = TimberTestRule.logAllAlways(); |
||||
|
|
||||
|
private static final String TIME_ZONE ="Europe/Athens"; |
||||
|
private static final String GET_DTZ ="getDtz"; |
||||
|
|
||||
|
private final String [] expTimestamps={"1569245936000","1569191627000","1570050809000"}; |
||||
|
private final String [][] dateTimes = { |
||||
|
{ |
||||
|
"Σεπτεμβρίου 23, 2019, 16:38:56", |
||||
|
"Σεπτεμβρίου 23, 2019, 16:38:56 pm", |
||||
|
"Σεπτεμβρίου 23, 2019, 04:38:56 pm", |
||||
|
"23 Σεπτεμβρίου 2019, 16:38:56", |
||||
|
"2019-09-23, 16:38:56", |
||||
|
"23-09-2019, 16:38:56" |
||||
|
}, |
||||
|
{ |
||||
|
"Σεπτεμβρίου 23, 2019, 01:33:47", |
||||
|
"Σεπτεμβρίου 23, 2019, 01:33:47 am", |
||||
|
"23 Σεπτεμβρίου 2019, 01:33:47", |
||||
|
"23-09-2019, 01:33:47", |
||||
|
"2019-09-23, 01:33:47" |
||||
|
}, |
||||
|
{ |
||||
|
"Οκτωβρίου 03, 2019, 12:13:29 am", |
||||
|
"Οκτωβρίου 03, 2019, 00:13:29 am" |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
@Test |
||||
|
public void dateTimesAreConvertedCorrectly() { |
||||
|
stub(method(ThmmyDateTimeParser.class, GET_DTZ)).toReturn(DateTimeZone.forID(TIME_ZONE)); |
||||
|
|
||||
|
String[][] expectedTimeStamps = new String[dateTimes.length][]; |
||||
|
String[][] timeStamps = new String[dateTimes.length][]; |
||||
|
|
||||
|
for(int i=0; i<dateTimes.length; i++){ |
||||
|
timeStamps[i] = new String[dateTimes[i].length]; |
||||
|
expectedTimeStamps[i] = new String[dateTimes[i].length]; |
||||
|
for(int j=0; j<dateTimes[i].length; j++){ |
||||
|
expectedTimeStamps[i][j]=expTimestamps[i]; |
||||
|
timeStamps[i][j]=convertToTimestamp(dateTimes[i][j]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
assertArrayEquals(expectedTimeStamps,timeStamps); |
||||
|
} |
||||
|
|
||||
|
private final String [] todayDateTimes = { |
||||
|
"10:10:10", |
||||
|
"00:58:07", |
||||
|
"23:23:23", |
||||
|
"09:09:09 am", |
||||
|
"09:09:09 pm" |
||||
|
}; |
||||
|
|
||||
|
@Test |
||||
|
public void todayDateTimeConvertToNonNull() { |
||||
|
stub(method(ThmmyDateTimeParser.class, GET_DTZ)).toReturn(DateTimeZone.forID(TIME_ZONE)); |
||||
|
|
||||
|
for (String todayDateTime : todayDateTimes) |
||||
|
assertNotNull(convertToTimestamp(todayDateTime)); |
||||
|
} |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
/build |
@ -0,0 +1,29 @@ |
|||||
|
apply plugin: 'com.android.library' |
||||
|
|
||||
|
android { |
||||
|
compileSdkVersion 29 |
||||
|
buildToolsVersion "29.0.2" |
||||
|
|
||||
|
|
||||
|
defaultConfig { |
||||
|
minSdkVersion 19 |
||||
|
targetSdkVersion 29 |
||||
|
versionCode 1 |
||||
|
versionName "1.0" |
||||
|
|
||||
|
consumerProguardFiles 'consumer-rules.pro' |
||||
|
} |
||||
|
|
||||
|
buildTypes { |
||||
|
release { |
||||
|
minifyEnabled false |
||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
dependencies { |
||||
|
implementation fileTree(dir: 'libs', include: ['*.jar']) |
||||
|
implementation 'androidx.appcompat:appcompat:1.1.0' |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
# Add project specific ProGuard rules here. |
||||
|
# You can control the set of applied configuration files using the |
||||
|
# proguardFiles setting in build.gradle. |
||||
|
# |
||||
|
# For more details, see |
||||
|
# http://developer.android.com/guide/developing/tools/proguard.html |
||||
|
|
||||
|
# If your project uses WebView with JS, uncomment the following |
||||
|
# and specify the fully qualified class name to the JavaScript interface |
||||
|
# class: |
||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { |
||||
|
# public *; |
||||
|
#} |
||||
|
|
||||
|
# Uncomment this to preserve the line number information for |
||||
|
# debugging stack traces. |
||||
|
#-keepattributes SourceFile,LineNumberTable |
||||
|
|
||||
|
# If you keep the line number information, uncomment this to |
||||
|
# hide the original source file name. |
||||
|
#-renamesourcefileattribute SourceFile |
@ -0,0 +1,2 @@ |
|||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
package="gr.thmmy.emojis" /> |
Before Width: | Height: | Size: 636 B After Width: | Height: | Size: 636 B |
Before Width: | Height: | Size: 603 B After Width: | Height: | Size: 603 B |
Before Width: | Height: | Size: 618 B After Width: | Height: | Size: 618 B |
Before Width: | Height: | Size: 630 B After Width: | Height: | Size: 630 B |
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 628 B |
Before Width: | Height: | Size: 630 B After Width: | Height: | Size: 630 B |
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 628 B |
Before Width: | Height: | Size: 630 B After Width: | Height: | Size: 630 B |
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 628 B |
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 629 B |
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 628 B |
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 629 B |
Before Width: | Height: | Size: 617 B After Width: | Height: | Size: 617 B |
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 628 B |
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 629 B |
Before Width: | Height: | Size: 633 B After Width: | Height: | Size: 633 B |
Before Width: | Height: | Size: 570 B After Width: | Height: | Size: 570 B |
Before Width: | Height: | Size: 614 B After Width: | Height: | Size: 614 B |
Before Width: | Height: | Size: 600 B After Width: | Height: | Size: 600 B |
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 643 B |
Before Width: | Height: | Size: 641 B After Width: | Height: | Size: 641 B |
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 643 B |
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 643 B |
Before Width: | Height: | Size: 603 B After Width: | Height: | Size: 603 B |
Before Width: | Height: | Size: 631 B After Width: | Height: | Size: 631 B |
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 643 B |
Before Width: | Height: | Size: 631 B After Width: | Height: | Size: 631 B |
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 643 B |
Before Width: | Height: | Size: 631 B After Width: | Height: | Size: 631 B |
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 643 B |
Before Width: | Height: | Size: 631 B After Width: | Height: | Size: 631 B |
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 643 B |
Before Width: | Height: | Size: 631 B After Width: | Height: | Size: 631 B |
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 643 B |
Before Width: | Height: | Size: 617 B After Width: | Height: | Size: 617 B |
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 628 B |
Before Width: | Height: | Size: 620 B After Width: | Height: | Size: 620 B |
Before Width: | Height: | Size: 565 B After Width: | Height: | Size: 565 B |
Before Width: | Height: | Size: 555 B After Width: | Height: | Size: 555 B |
Before Width: | Height: | Size: 622 B After Width: | Height: | Size: 622 B |
Before Width: | Height: | Size: 604 B After Width: | Height: | Size: 604 B |
Before Width: | Height: | Size: 573 B After Width: | Height: | Size: 573 B |
Before Width: | Height: | Size: 605 B After Width: | Height: | Size: 605 B |
Before Width: | Height: | Size: 536 B After Width: | Height: | Size: 536 B |
Before Width: | Height: | Size: 517 B After Width: | Height: | Size: 517 B |
Before Width: | Height: | Size: 614 B After Width: | Height: | Size: 614 B |
Before Width: | Height: | Size: 527 B After Width: | Height: | Size: 527 B |
Before Width: | Height: | Size: 516 B After Width: | Height: | Size: 516 B |
Before Width: | Height: | Size: 526 B After Width: | Height: | Size: 526 B |
Before Width: | Height: | Size: 517 B After Width: | Height: | Size: 517 B |
Before Width: | Height: | Size: 517 B After Width: | Height: | Size: 517 B |
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 465 B |
Before Width: | Height: | Size: 466 B After Width: | Height: | Size: 466 B |
Before Width: | Height: | Size: 534 B After Width: | Height: | Size: 534 B |
Before Width: | Height: | Size: 521 B After Width: | Height: | Size: 521 B |
Before Width: | Height: | Size: 525 B After Width: | Height: | Size: 525 B |
Before Width: | Height: | Size: 618 B After Width: | Height: | Size: 618 B |