/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.holo.fileexplorer;
import com.holo.actions.FileActionSupport;
import com.holo.fileexplorer.R;
import java.io.File;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.Layout.Alignment;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import com.google.common.base.Objects;
//import com.android.email.R;
//import com.android.emailcommon.utility.TextUtilities;
//import com.google.common.base.Objects;
/**
* This custom View is the list item for the MessageList activity, and serves
* two purposes: 1. It's a container to store message metadata (e.g. the ids of
* the message, mailbox, & account) 2. It handles internal clicks such as the
* checkbox or the favorite star
*/
public class FileListItem extends View {
// private ThreePaneLayout mLayout;
private FilesAdapter mAdapter;
private FileListItemCoordinates mCoordinates;
private Context mContext;
private FileMeta mFileMeta;
private boolean mDownEvent;
public static final String MESSAGE_LIST_ITEMS_CLIP_LABEL = "com.android.email.MESSAGE_LIST_ITEMS";
public FileListItem(Context context, FileMeta fileMeta) {
super(context);
mContext = context;
mFileMeta = fileMeta;
init(context);
}
public FileListItem(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public FileListItem(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
// Wide mode shows name, date, time, and favorite spread out across the
// screen
private static final int MODE_WIDE = FileListItemCoordinates.WIDE_MODE;
// Sentinel indicating that the view needs layout
public static final int NEEDS_LAYOUT = -1;
private static boolean sInit = false;
private static final TextPaint sDefaultPaint = new TextPaint();
private static final TextPaint sBoldPaint = new TextPaint();
private static final TextPaint sDatePaint = new TextPaint();
private static Bitmap sSelectedIconOn;
private static Bitmap sSelectedIconOff;
private static String sDetailDateDivider;
private static String sDetailDescription;
private static String sDetailEmptyDescription;
// Static colors.
private static int DETAIL_TEXT_COLOR;
private static int DATE_TEXT_COLOR;
private static int FILE_NAME_TEXT_COLOR;
public String mSender;
public SpannableStringBuilder mText;
public CharSequence mFileDetailText;
private String mFileDetail;
private CharSequence mModifiedDate;
private StaticLayout mDetailLayout;
public boolean mRead = false;
public boolean mHasAttachment = false;
public boolean mHasInvite = false;
public boolean mIsFavorite = false;
public boolean mHasBeenRepliedTo = false;
public boolean mHasBeenForwarded = false;
/** {@link Paint} for account color chips. null if no chips should be drawn. */
public Paint mColorChipPaint;
private int mMode = -1;
private int mViewWidth = 0;
private int mViewHeight = 0;
private static int sItemHeightWide;
private static int sItemHeightNormal;
private static int sIconDimension;
// Note: these cannot be shared Drawables because they are selectors which
// have state.
private Drawable mReadSelector;
private Drawable mUnreadSelector;
private Drawable mWideReadSelector;
private Drawable mWideUnreadSelector;
private CharSequence mFormattedFileName;
// We must initialize this to something, in case the timestamp of the
// message is zero (which
// should be very rare); this is otherwise set in setTimestamp
private CharSequence mFormattedDate = "";
private void init(Context context) {
mContext = context;
if (!sInit) {
Resources r = context.getResources();
final TypedArray a = context.getTheme().obtainStyledAttributes(
R.styleable.AppTheme);
sDetailDescription = r.getString(R.string.file_detail_description)
.concat(", ");
sDetailEmptyDescription = r
.getString(R.string.message_is_empty_description);
sDetailDateDivider = r
.getString(R.string.file_list_detail_date_divider);
sItemHeightWide = r
.getDimensionPixelSize(R.dimen.message_list_item_height_wide);
sItemHeightNormal = r
.getDimensionPixelSize(R.dimen.message_list_item_height_normal);
sIconDimension = r
.getDimensionPixelSize(R.dimen.file_icon_dimension);
sDefaultPaint.setTypeface(Typeface.DEFAULT);
sDefaultPaint.setAntiAlias(true);
sDatePaint.setTypeface(Typeface.DEFAULT);
sDatePaint.setAntiAlias(true);
sBoldPaint.setTypeface(Typeface.DEFAULT_BOLD);
sBoldPaint.setAntiAlias(true);
sSelectedIconOff = BitmapFactory.decodeResource(r, a.getResourceId(
R.styleable.AppTheme_buttonCheckOff,
R.drawable.btn_check_off_normal_holo_light));
sSelectedIconOn = BitmapFactory.decodeResource(r, a.getResourceId(
R.styleable.AppTheme_buttonCheckOn,
R.drawable.btn_check_on_normal_holo_light));
// sSelectedIconOff = BitmapFactory.decodeResource(r,
// R.drawable.btn_check_off_normal_holo_light);
// sSelectedIconOn = BitmapFactory.decodeResource(r,
// R.drawable.btn_check_on_normal_holo_light);
FILE_NAME_TEXT_COLOR = a.getColor(
R.styleable.AppTheme_listItemFileNameTextColor,
R.color.text_test);
DETAIL_TEXT_COLOR = a.getColor(
R.styleable.AppTheme_listItemDetailTextColor,
R.color.text_test);
DATE_TEXT_COLOR = a.getColor(
R.styleable.AppTheme_listItemDateTextColor,
R.color.text_test);
sInit = true;
}
// TODO: just move thise all to a MessageListItem.bindTo(cursor) so that
// the fields can
// be private, and their inter-dependence when they change can be
// abstracted away.
// Load the public fields in the view (for later use)
// itemView.mMessageId = cursor.getLong(COLUMN_ID);
// itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY);
// final long accountId = cursor.getLong(COLUMN_ACCOUNT_KEY);
// itemView.mAccountId = accountId;
//
// boolean isRead = cursor.getInt(COLUMN_READ) != 0;
// boolean readChanged = isRead != itemView.mRead;
// itemView.mRead = isRead;
// itemView.mIsFavorite = cursor.getInt(COLUMN_FAVORITE) != 0;
// final int flags = cursor.getInt(COLUMN_FLAGS);
// itemView.mHasInvite = (flags & Message.FLAG_INCOMING_MEETING_INVITE)
// != 0;
// itemView.mHasBeenRepliedTo = (flags & Message.FLAG_REPLIED_TO) != 0;
// itemView.mHasBeenForwarded = (flags & Message.FLAG_FORWARDED) != 0;
// itemView.mHasAttachment = cursor.getInt(COLUMN_ATTACHMENTS) != 0;
// itemView.setTimestamp(cursor.getLong(COLUMN_DATE));
// itemView.mSender = cursor.getString(COLUMN_DISPLAY_NAME);
// itemView.setText(
// cursor.getString(COLUMN_DETAIL), cursor.getString(COLUMN_DATE),
// readChanged);
// itemView.mColorChipPaint =
// mShowColorChips ? mResourceHelper.getAccountColorPaint(accountId) :
// null;
//
// if (mQuery != null && itemView.mDate != null) {
// itemView.mDate =
// TextUtilities.highlightTermsInText(cursor.getString(COLUMN_DATE),
// mQuery);
// }
setText(false);
mSender = mFileMeta.mName;
LayoutInflater mInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mInflater.inflate(R.layout.file_list_item_normal, null);
}
public String getIdentifer() {
// TODO Auto-generated method stub
return mFileMeta.mPath;
}
public void reInit(FileMeta fileMeta) {
mFileMeta = fileMeta;
init(mContext);
}
/**
* Invalidate all drawing caches associated with drawing message list items.
* This is an expensive operation, and should be done rarely, such as when
* system font size changes occurs.
*/
public static void resetDrawingCaches() {
FileListItemCoordinates.resetCaches();
sInit = false;
}
/**
* Sets message detail and date safely, ensuring the cache is invalidated.
*/
public void setText(boolean forceUpdate) {
boolean changed = false;
if (!Objects.equal(mFileDetailText, mFileMeta.mDetail)) {
mFileDetailText = mFileMeta.mDetail;
changed = true;
populateContentDescription();
}
if (!Objects.equal(mFileDetailText, mFileMeta.mPath)) {
// mFileDetail = mFileMeta.mPath;
if (mFileMeta.mModifiedDate != null) {
// "parent directory" listview item will have a date of null, so
// do this to avoid nullPointerExceptions
mModifiedDate = android.text.format.DateFormat.format(
"MMM d, yyyy h:mmaa", mFileMeta.mModifiedDate);
}
// mModifiedDate = dateFormat.format(mFileMeta.mModifiedDate);
changed = true;
}
if (forceUpdate || changed
|| (mFileDetailText == null && mFileDetailText == null) /*
* first
* time
*/) {
SpannableStringBuilder ssb = new SpannableStringBuilder();
boolean hasDetail = false;
if (!TextUtils.isEmpty(mFileDetailText)) {
SpannableString ss = new SpannableString(mFileDetailText);
ss.setSpan(new StyleSpan(Typeface.BOLD), 0, ss.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.append(ss);
hasDetail = true;
}
// Temporary(?) switch from file location detail to date modified
// if (!TextUtils.isEmpty(mFileDetail)) {
// if (hasDetail) {
// ssb.append(sDetailDateDivider);
// }
// ssb.append(mFileDetail);
// }
if (!TextUtils.isEmpty(mModifiedDate)) {
if (hasDetail) {
ssb.append(" " + sDetailDateDivider + " ");
}
ssb.append(mModifiedDate);
}
mText = ssb;
requestLayout();
}
}
long mTimeFormatted = 0;
public void setTimestamp(long timestamp) {
if (mTimeFormatted != timestamp) {
mFormattedDate = DateUtils.getRelativeTimeSpanString(mContext,
timestamp).toString();
mTimeFormatted = timestamp;
}
}
/**
* Determine the mode of this view (WIDE or NORMAL)
*
* @param width
* The width of the view
* @return The mode of the view
*/
private int getViewMode(int width) {
return FileListItemCoordinates.getMode(mContext, width);
}
private Drawable mCurentBackground = null; // Only used by
// updateBackground()
private void updateBackground() {
final Drawable newBackground;
boolean isMultiPane = FileListItemCoordinates.isMultiPane(mContext);
if (false) { // mRead
// if (isMultiPane && mLayout.isLeftPaneVisible()) {
// if (mWideReadSelector == null) {
// mWideReadSelector = getContext().getResources()
// .getDrawable(R.drawable.conversation_wide_read_selector);
// }
// newBackground = mWideReadSelector;
// } else {
if (mReadSelector == null) {
mReadSelector = getContext().getResources().getDrawable(
R.drawable.item_selector);
}
newBackground = mReadSelector;
// }
} else {
// if (isMultiPane && mLayout.isLeftPaneVisible()) {
// if (mWideUnreadSelector == null) {
// mWideUnreadSelector = getContext().getResources().getDrawable(
// R.drawable.conversation_wide_unread_selector);
// }
// newBackground = mWideUnreadSelector;
// } else {
if (mUnreadSelector == null) {
mUnreadSelector = getContext().getResources().getDrawable(
R.drawable.item_selector);
}
newBackground = mUnreadSelector;
// }
}
if (newBackground != mCurentBackground) {
// setBackgroundDrawable is a heavy operation. Only call it when
// really needed.
setBackgroundDrawable(newBackground);
mCurentBackground = newBackground;
}
}
private void calculateDetailText() {
if (mText == null || mText.length() == 0) {
return;
}
boolean hasDetail = false;
int dateStart = 0;
if (!TextUtils.isEmpty(mFileDetailText)) {
int detailColor = getFontColor(DETAIL_TEXT_COLOR);
mText.setSpan(new ForegroundColorSpan(detailColor), 0,
mFileDetailText.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
dateStart = mFileDetailText.length() + 1;
}
if (!TextUtils.isEmpty(mFileDetailText)) {
int dateColor = getFontColor(DATE_TEXT_COLOR);
mText.setSpan(new ForegroundColorSpan(dateColor), dateStart,
mText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
private void calculateDrawingData() {
sDefaultPaint.setTextSize(mCoordinates.detailFontSize);
calculateDetailText();
// mText
mDetailLayout = new StaticLayout(mText, sDefaultPaint,
mCoordinates.detailWidth, Alignment.ALIGN_NORMAL, 1, 0, false /* includePad */);
if (mCoordinates.detailLineCount < mDetailLayout.getLineCount()) {
// TODO: ellipsize.
int end = mDetailLayout
.getLineEnd(mCoordinates.detailLineCount - 1);
mDetailLayout = new StaticLayout(mText.subSequence(0, end),
sDefaultPaint, mCoordinates.detailWidth,
Alignment.ALIGN_NORMAL, 1, 0, true);
}
// Now, format the name for its width
TextPaint namePaint = mRead ? sDefaultPaint : sBoldPaint;
// And get the ellipsized string for the calculated width
if (TextUtils.isEmpty(mSender)) {
mFormattedFileName = "";
} else {
int nameWidth = mCoordinates.nameWidth;
namePaint.setTextSize(mCoordinates.nameFontSize);
namePaint.setColor(getFontColor(FILE_NAME_TEXT_COLOR));
mFormattedFileName = TextUtils.ellipsize(mSender, namePaint,
nameWidth, TruncateAt.END);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (widthMeasureSpec != 0 || mViewWidth == 0) {
mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
int mode = getViewMode(mViewWidth);
if (mode != mMode) {
mMode = mode;
}
mViewHeight = measureHeight(heightMeasureSpec, mMode);
}
setMeasuredDimension(mViewWidth, mViewHeight);
}
/**
* Determine the height of this view
*
* @param measureSpec
* A measureSpec packed into an int
* @param mode
* The current mode of this view
* @return The height of the view, honoring constraints from measureSpec
*/
private int measureHeight(int measureSpec, int mode) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
} else {
// Measure the text
if (mMode == MODE_WIDE) {
result = sItemHeightWide;
} else {
result = sItemHeightNormal;
}
if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by
// measureSpec
result = Math.min(result, specSize);
}
}
return result;
}
@Override
public void draw(Canvas canvas) {
// Update the background, before View.draw() draws it.
setSelected(mAdapter.isSelected(this));
updateBackground();
super.draw(canvas);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
mCoordinates = FileListItemCoordinates.forWidth(mContext, mViewWidth);
calculateDrawingData();
}
private int getFontColor(int defaultColor) {
// return isActivated() ? ACTIVATED_TEXT_COLOR : defaultColor;
return defaultColor;
}
@Override
protected void onDraw(Canvas canvas) {
// Draw the color chip indicating the mailbox this belongs to
// if (mColorChipPaint != null) {
// canvas.drawRect(mCoordinates.chipX, mCoordinates.chipY,
// mCoordinates.chipX + mCoordinates.chipWidth, mCoordinates.chipY
// + mCoordinates.chipHeight, mColorChipPaint);
// }
// Draw the icon
Drawable icon = FileActionSupport.getIcon(mContext, new File(
mFileMeta.mPath));
if (icon != null) {
icon.setBounds(mCoordinates.iconX, mCoordinates.iconY,
mCoordinates.iconX + sIconDimension, mCoordinates.iconY
+ sIconDimension);
icon.draw(canvas);
}
// sFileIcon = BitmapFactory.decodeResource(mContext.getResources(),
// fileId);
// canvas.drawBitmap(iconBitmap, mCoordinates.iconX, mCoordinates.iconY,
// null);
// Draw the checkbox
canvas.drawBitmap(mAdapter.isSelected(this) ? sSelectedIconOn
: sSelectedIconOff, mCoordinates.checkmarkX,
mCoordinates.checkmarkY, null);
// Draw the file name
Paint fileNamePaint = sBoldPaint; // mRead ? sDefaultPaint : sBoldPaint
fileNamePaint.setColor(getFontColor(FILE_NAME_TEXT_COLOR));
fileNamePaint.setTextSize(mCoordinates.nameFontSize);
canvas.drawText(mFormattedFileName, 0, mFormattedFileName.length(),
mCoordinates.nameX, mCoordinates.nameY
- mCoordinates.nameAscent, fileNamePaint);
// Detail and date.
sDefaultPaint.setTextSize(mCoordinates.detailFontSize);
canvas.save();
canvas.translate(mCoordinates.detailX, mCoordinates.detailY);
mDetailLayout.draw(canvas);
canvas.restore();
// Draw the date
sDatePaint.setTextSize(mCoordinates.dateFontSize);
sDatePaint.setColor(DATE_TEXT_COLOR);
int dateX = mCoordinates.dateXEnd
- (int) sDatePaint.measureText(mFormattedDate, 0,
mFormattedDate.length());
canvas.drawText(mFormattedDate, 0, mFormattedDate.length(), dateX,
mCoordinates.dateY - mCoordinates.dateAscent, sDatePaint);
// Draw the favorite icon
// TODO: deal with the icon layouts better from the coordinate class so
// that this logic
// doesn't have to exist.
// Draw the attachment and invite icons, if necessary.
// int iconsLeft = dateX - sBadgeMargin;
// if (mHasAttachment) {
// iconsLeft = iconsLeft - sAttachmentIcon.getWidth();
// canvas.drawBitmap(sAttachmentIcon, iconsLeft,
// mCoordinates.paperclipY, null);
// }
// if (mHasInvite) {
// iconsLeft -= sInviteIcon.getWidth();
// canvas.drawBitmap(sInviteIcon, iconsLeft, mCoordinates.paperclipY,
// null);
// }
}
public static int getIcon(File file) {
if (!file.isFile()) // dir
{
if (FileActionSupport.isProtected(file)) {
return (R.drawable.file_extension_dir_sys);
} else if (FileActionSupport.isSdCard(file)) {
return (R.drawable.file_extension_dir_sdcard);
} else {
return (R.drawable.file_extension_dir);
}
} else // file
{
String fileName = file.getName();
if (FileActionSupport.isProtected(file)) {
return (R.drawable.file_extension_error);
}
if (fileName.endsWith(".apk")) {
return (R.drawable.file_extension_generic);
}
if (fileName.endsWith(".zip")) {
return (R.drawable.file_extension_zip);
}
// else if(FileActionSupport.isMusic(file))
// {
// return (R.drawable.file_extension_generic);
// }
// else if(FileActionSupport.isVideo(file))
// {
// return (R.drawable.file_extension_mpg);
// }
else if (FileActionSupport.isPicture(file)) {
return (R.drawable.file_extension_jpg);
} else {
return (R.drawable.file_extension_generic);
}
}
}
/**
* Called by the adapter at bindView() time
*
* @param adapter
* the adapter that creates this view
* @param layout
* If this is a three pane implementation, the ThreePaneLayout.
* Otherwise, null.
*/
public void bindViewInit(FilesAdapter adapter/* , ThreePaneLayout layout */) {
// mLayout = layout;
mAdapter = adapter;
requestLayout();
}
private static final int TOUCH_SLOP = 24;
private static int sScaledTouchSlop = -1;
private void initializeSlop(Context context) {
if (sScaledTouchSlop == -1) {
final Resources res = context.getResources();
final Configuration config = res.getConfiguration();
final float density = res.getDisplayMetrics().density;
final float sizeAndDensity;
// TODO Pre Honeycomb devices will FC on the second condition, hence
// the addition of the first. Not quite sure how this
// code now treats pre-Honecomb large screen devices (like the fire)
// but it probably isn't good. Need a way to safely determine
// screen size.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
&& config
.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_XLARGE)) {
sizeAndDensity = density * 1.5f;
} else {
sizeAndDensity = density;
}
sScaledTouchSlop = (int) (sizeAndDensity * TOUCH_SLOP + 0.5f);
}
}
/**
* Overriding this method allows us to "catch" clicks in the checkbox or
* star and process them accordingly.
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
initializeSlop(getContext());
boolean handled = false;
int touchX = (int) event.getX();
// int checkRight = mCoordinates.checkmarkX
// + mCoordinates.checkmarkWidthIncludingMargins
// + sScaledTouchSlop;
int checkRight = mCoordinates.checkmarkX - sScaledTouchSlop;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (touchX > checkRight) {
mDownEvent = true;
if (touchX > checkRight) {
handled = true;
}
}
break;
case MotionEvent.ACTION_CANCEL:
mDownEvent = false;
break;
case MotionEvent.ACTION_UP:
if (mDownEvent) {
if (touchX > checkRight) {
mAdapter.toggleSelected(this);
handled = true;
// } else if (touchX > starLeft) {
// mIsFavorite = !mIsFavorite;
// mAdapter.updateFavorite(this, mIsFavorite);
// handled = true;
}
}
break;
}
if (handled) {
invalidate();
} else {
handled = super.onTouchEvent(event);
}
return handled;
}
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
event.setClassName(getClass().getName());
event.setPackageName(getContext().getPackageName());
event.setEnabled(true);
event.setContentDescription(getContentDescription());
return true;
}
/**
* Sets the content description for this item, used for accessibility.
*/
private void populateContentDescription() {
if (!TextUtils.isEmpty(mFileDetailText)) {
setContentDescription(sDetailDescription + mFileDetailText);
} else {
setContentDescription(sDetailEmptyDescription);
}
}
}