@ -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 |