mirror of https://github.com/ThmmyNoLife/mTHMMY
Ezerous
5 years ago
22 changed files with 631 additions and 80 deletions
@ -0,0 +1,64 @@ |
|||||
|
package gr.thmmy.mthmmy.utils; |
||||
|
|
||||
|
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; |
||||
|
|
||||
|
public class DateTimeUtils { |
||||
|
//TODO: move this function to ThmmyDateTimeParser class once KitKat support is dropped
|
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
private static final long MONTH_IN_MILLIS = DAY_IN_MILLIS*30; |
||||
|
private static final long DECADE_IN_MILLIS = YEAR_IN_MILLIS*10; |
||||
|
|
||||
|
static CharSequence getRelativeTimeSpanString(long time, long now, long minResolution) { |
||||
|
boolean past = (now >= time); |
||||
|
long duration = Math.abs(now - time); |
||||
|
String format; |
||||
|
long count; |
||||
|
if (duration < MINUTE_IN_MILLIS && minResolution < MINUTE_IN_MILLIS) { |
||||
|
count = duration / SECOND_IN_MILLIS; |
||||
|
format = "%d sec"; |
||||
|
} else if (duration < HOUR_IN_MILLIS && minResolution < HOUR_IN_MILLIS) { |
||||
|
count = duration / MINUTE_IN_MILLIS; |
||||
|
format = "%d min"; |
||||
|
} else if (duration < DAY_IN_MILLIS && minResolution < DAY_IN_MILLIS) { |
||||
|
count = duration / HOUR_IN_MILLIS; |
||||
|
format = "%d hour"; |
||||
|
if(count>1) |
||||
|
format = format + 's'; |
||||
|
} else if (duration < MONTH_IN_MILLIS && minResolution < MONTH_IN_MILLIS) { |
||||
|
count = duration / DAY_IN_MILLIS; |
||||
|
format = "%d day"; |
||||
|
if(count>1) |
||||
|
format = format + 's'; |
||||
|
} else if (duration < YEAR_IN_MILLIS && minResolution < YEAR_IN_MILLIS) { |
||||
|
count = duration / MONTH_IN_MILLIS; |
||||
|
format = "%d month"; |
||||
|
if(count>1) |
||||
|
format = format + 's'; |
||||
|
} else if (duration < DECADE_IN_MILLIS && minResolution < DECADE_IN_MILLIS) { |
||||
|
count = duration / YEAR_IN_MILLIS; |
||||
|
format = "%d year"; |
||||
|
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,249 @@ |
|||||
|
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(getRelativeTimeDisplayString(mReferenceTime, System.currentTimeMillis())); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the text to display for relative time. |
||||
|
* <br/> |
||||
|
* @param referenceTime The reference time passed in through {@link #setReferenceTime(long)} or through {@code reference_time} attribute |
||||
|
* @param now The current time |
||||
|
* @return The display text for the relative time |
||||
|
*/ |
||||
|
protected CharSequence getRelativeTimeDisplayString(long referenceTime, long now) { |
||||
|
long difference = now - referenceTime; |
||||
|
return (difference >= 0 && difference<=DateUtils.MINUTE_IN_MILLIS) ? |
||||
|
"just now" : |
||||
|
getRelativeTimeSpanString( |
||||
|
mReferenceTime, |
||||
|
now, |
||||
|
DateUtils.MINUTE_IN_MILLIS); |
||||
|
} |
||||
|
|
||||
|
@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,63 @@ |
|||||
|
package gr.thmmy.mthmmy.utils.parsing; |
||||
|
|
||||
|
|
||||
|
import android.os.Build; |
||||
|
|
||||
|
import androidx.annotation.RequiresApi; |
||||
|
|
||||
|
import org.joda.time.DateTime; |
||||
|
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.util.Locale; |
||||
|
|
||||
|
import gr.thmmy.mthmmy.base.BaseApplication; |
||||
|
import timber.log.Timber; |
||||
|
|
||||
|
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) |
||||
|
public class ThmmyDateTimeParser { |
||||
|
private static final DateTimeParser[] parsers = { |
||||
|
DateTimeFormat.forPattern("HH:mm:ss").getParser(), |
||||
|
DateTimeFormat.forPattern("HH:mm:ss a").getParser(), |
||||
|
DateTimeFormat.forPattern("MMMM d, Y, HH:mm:ss").getParser(), |
||||
|
DateTimeFormat.forPattern("MMMM d, Y, HH:mm:ss a").getParser() |
||||
|
}; |
||||
|
|
||||
|
private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder() |
||||
|
.append(null, parsers) |
||||
|
.toFormatter(); |
||||
|
|
||||
|
private static final Locale greekLocale = Locale.forLanguageTag("el-GR"); |
||||
|
private static final Locale englishLocale = Locale.forLanguageTag("en-US"); |
||||
|
|
||||
|
public static String convertToTimestamp(String thmmyDateTime){ |
||||
|
DateTimeZone dtz; |
||||
|
if(!BaseApplication.getInstance().getSessionManager().isLoggedIn()) |
||||
|
dtz = DateTimeZone.forID("Europe/Athens"); |
||||
|
else |
||||
|
dtz = DateTimeZone.getDefault(); |
||||
|
|
||||
|
//Add today's date for the first two cases
|
||||
|
if(Character.isDigit(thmmyDateTime.charAt(0))) |
||||
|
thmmyDateTime = (new DateTime()).toString("MMMM d, Y, ") + thmmyDateTime; |
||||
|
|
||||
|
DateTime dateTime; |
||||
|
try{ |
||||
|
dateTime=formatter.withZone(dtz).withLocale(greekLocale).parseDateTime(thmmyDateTime); |
||||
|
} |
||||
|
catch (IllegalArgumentException e1){ |
||||
|
Timber.i("Parsing DateTime using Greek Locale failed."); |
||||
|
try{ |
||||
|
dateTime=formatter.withZone(dtz).withLocale(englishLocale).parseDateTime(thmmyDateTime); |
||||
|
} |
||||
|
catch (IllegalArgumentException e2){ |
||||
|
Timber.e("Couldn't parse DateTime %s", thmmyDateTime); |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
return Long.toString(dateTime.getMillis()); |
||||
|
} |
||||
|
} |
@ -0,0 +1,43 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"> |
||||
|
|
||||
|
<androidx.preference.PreferenceCategory |
||||
|
android:title="@string/pref_category_app" |
||||
|
app:iconSpaceReserved="false"> |
||||
|
<androidx.preference.ListPreference |
||||
|
android:defaultValue="0" |
||||
|
android:dialogTitle="@string/pref_app_main_default_tab_dialog_title" |
||||
|
android:entries="@array/pref_app_main_default_tab_entries" |
||||
|
android:entryValues="@array/pref_app_main_default_tab_values" |
||||
|
android:key="@string/pref_app_main_default_tab_key" |
||||
|
android:title="@string/pref_title_app_main_default_tab" |
||||
|
android:summary="@string/pref_summary_app_main_default_tab" |
||||
|
app:iconSpaceReserved="false" /> |
||||
|
<androidx.preference.SwitchPreferenceCompat |
||||
|
android:defaultValue="false" |
||||
|
android:key="@string/pref_app_display_relative_time_key" |
||||
|
android:title="@string/pref_title_display_relative_time" |
||||
|
android:summary="@string/pref_summary_display_relative_time" |
||||
|
app:iconSpaceReserved="false" /> |
||||
|
</androidx.preference.PreferenceCategory> |
||||
|
|
||||
|
<androidx.preference.PreferenceCategory |
||||
|
android:key="@string/pref_category_privacy_key" |
||||
|
android:title="@string/pref_category_privacy" |
||||
|
app:iconSpaceReserved="false"> |
||||
|
<androidx.preference.SwitchPreferenceCompat |
||||
|
android:defaultValue="false" |
||||
|
android:key="@string/pref_privacy_crashlytics_enable_key" |
||||
|
android:title="@string/pref_title_privacy_crashlytics_enable" |
||||
|
android:summary="@string/pref_summary_privacy_crashlytics_enable" |
||||
|
app:iconSpaceReserved="false" /> |
||||
|
<androidx.preference.SwitchPreferenceCompat |
||||
|
android:defaultValue="false" |
||||
|
android:key="@string/pref_privacy_analytics_enable_key" |
||||
|
android:title="@string/pref_title_privacy_analytics_enable" |
||||
|
android:summary="@string/pref_summary_privacy_analytics_enable" |
||||
|
app:iconSpaceReserved="false" /> |
||||
|
</androidx.preference.PreferenceCategory> |
||||
|
|
||||
|
</androidx.preference.PreferenceScreen> |
@ -0,0 +1,66 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"> |
||||
|
|
||||
|
<androidx.preference.PreferenceCategory |
||||
|
android:title="@string/pref_category_app" |
||||
|
app:iconSpaceReserved="false"> |
||||
|
<androidx.preference.ListPreference |
||||
|
android:defaultValue="0" |
||||
|
android:dialogTitle="@string/pref_app_main_default_tab_dialog_title" |
||||
|
android:entries="@array/pref_app_main_default_tab_entries" |
||||
|
android:entryValues="@array/pref_app_main_default_tab_values" |
||||
|
android:key="@string/pref_app_main_default_tab_key" |
||||
|
android:title="@string/pref_title_app_main_default_tab" |
||||
|
android:summary="@string/pref_summary_app_main_default_tab" |
||||
|
app:iconSpaceReserved="false" /> |
||||
|
<androidx.preference.SwitchPreferenceCompat |
||||
|
android:defaultValue="false" |
||||
|
android:key="@string/pref_app_display_relative_time_key" |
||||
|
android:title="@string/pref_title_display_relative_time" |
||||
|
android:summary="@string/pref_summary_display_relative_time" |
||||
|
app:iconSpaceReserved="false" /> |
||||
|
</androidx.preference.PreferenceCategory> |
||||
|
|
||||
|
<androidx.preference.PreferenceCategory |
||||
|
android:key="@string/pref_category_posting_key" |
||||
|
android:title="@string/pref_category_posting" |
||||
|
app:iconSpaceReserved="false"> |
||||
|
<androidx.preference.SwitchPreferenceCompat |
||||
|
android:defaultValue="true" |
||||
|
android:key="@string/pref_posting_app_signature_enable_key" |
||||
|
android:title="@string/pref_title_posting_app_signature_enable" |
||||
|
android:summary="@string/pref_summary_posting_app_signature_enable" |
||||
|
app:iconSpaceReserved="false" /> |
||||
|
</androidx.preference.PreferenceCategory> |
||||
|
|
||||
|
<androidx.preference.PreferenceCategory |
||||
|
android:key="@string/pref_category_uploading_key" |
||||
|
android:title="@string/pref_category_uploading" |
||||
|
app:iconSpaceReserved="false"> |
||||
|
<androidx.preference.SwitchPreferenceCompat |
||||
|
android:defaultValue="true" |
||||
|
android:key="@string/pref_uploading_app_signature_enable_key" |
||||
|
android:title="@string/pref_title_uploading_app_signature_enable" |
||||
|
android:summary="@string/pref_summary_uploading_app_signature_enable" |
||||
|
app:iconSpaceReserved="false" /> |
||||
|
</androidx.preference.PreferenceCategory> |
||||
|
|
||||
|
<androidx.preference.PreferenceCategory |
||||
|
android:key="@string/pref_category_privacy_key" |
||||
|
android:title="@string/pref_category_privacy" |
||||
|
app:iconSpaceReserved="false"> |
||||
|
<androidx.preference.SwitchPreferenceCompat |
||||
|
android:defaultValue="false" |
||||
|
android:key="@string/pref_privacy_crashlytics_enable_key" |
||||
|
android:title="@string/pref_title_privacy_crashlytics_enable" |
||||
|
android:summary="@string/pref_summary_privacy_crashlytics_enable" |
||||
|
app:iconSpaceReserved="false" /> |
||||
|
<androidx.preference.SwitchPreferenceCompat |
||||
|
android:defaultValue="false" |
||||
|
android:key="@string/pref_privacy_analytics_enable_key" |
||||
|
android:title="@string/pref_title_privacy_analytics_enable" |
||||
|
android:summary="@string/pref_summary_privacy_analytics_enable" |
||||
|
app:iconSpaceReserved="false" /> |
||||
|
</androidx.preference.PreferenceCategory> |
||||
|
</androidx.preference.PreferenceScreen> |
Loading…
Reference in new issue