package com.github.curioustechizen.ago; 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 io.github.mthli.Ninja.R; /** * A {@code TextView} that, given a reference time, renders that time as a time period relative to the current time. * @author Kiran Rao * @see #setReferenceTime(long) * */ public class RelativeTimeTextView extends TextView { private long mReferenceTime; private String mText; private String mPrefix; private String mSuffix; 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); try { mText = a.getString(R.styleable.RelativeTimeTextView_referenceTime); mPrefix = a.getString(R.styleable.RelativeTimeTextView_relativeTimePrefix); mSuffix = a.getString(R.styleable.RelativeTimeTextView_relativeTimeSuffix); mPrefix = mPrefix == null ? "" : mPrefix; mSuffix = mSuffix == null ? "" : mSuffix; } finally { a.recycle(); } try { mReferenceTime = Long.valueOf(mText); } catch (NumberFormatException nfe) { /* * TODO: Better exception handling */ mReferenceTime = -1L; } setReferenceTime(mReferenceTime); } /** * Returns prefix * @return */ public String getPrefix() { return this.mPrefix; } /** * String to be attached before the reference time * @param prefix * * Example: * [prefix] in XX minutes */ public void setPrefix(String prefix) { this.mPrefix = prefix; updateTextDisplay(); } /** * Returns suffix * @return */ public String getSuffix() { return this.mSuffix; } /** * String to be attached after the reference time * @param suffix * * Example: * in XX minutes [suffix] */ public void setSuffix(String suffix) { this.mSuffix = suffix; updateTextDisplay(); } /** * 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 */ mUpdateTimeTask = new UpdateTimeRunnable(mReferenceTime); /* * 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(mPrefix + getRelativeTimeDisplayString() + mSuffix); } private CharSequence getRelativeTimeDisplayString() { long now = System.currentTimeMillis(); long difference = now - mReferenceTime; return (difference >= 0 && difference<=DateUtils.MINUTE_IN_MILLIS) ? getResources().getString(R.string.android_ago_just_now): DateUtils.getRelativeTimeSpanString( mReferenceTime, now, DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE); } @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() { mHandler.post(mUpdateTimeTask); isUpdateTaskRunning = true; } private void stopTaskForPeriodicallyUpdatingRelativeTime() { if(isUpdateTaskRunning) { 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 class UpdateTimeRunnable implements Runnable{ private long mRefTime; UpdateTimeRunnable(long refTime){ this.mRefTime = refTime; } @Override public void run() { long difference = Math.abs(System.currentTimeMillis() - mRefTime); long interval = DateUtils.MINUTE_IN_MILLIS; 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; } updateTextDisplay(); mHandler.postDelayed(this, interval); } } }