content: /**
   * Copyright 2015-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * A copy of the License is located at
   *
   *     http://aws.amazon.com/apache2.0/
   *
   * or in the "license" file accompanying this file. This file 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.amazon.android.ui.widget;
  
  
  import com.amazon.android.ui.fragments.ReadDialogFragment;
  import com.amazon.android.ui.interfaces.SingleViewProvider;
  import com.amazon.utils.R;
  
  import android.annotation.SuppressLint;
  import android.app.Activity;
  import android.app.FragmentTransaction;
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.graphics.drawable.Drawable;
  import android.graphics.drawable.StateListDrawable;
  import android.os.Bundle;
  import android.support.v4.content.ContextCompat;
  import android.text.Layout;
  import android.text.Spannable;
  import android.text.SpannableString;
  import android.text.SpannableStringBuilder;
  import android.text.Spanned;
  import android.text.style.DynamicDrawableSpan;
  import android.text.style.ImageSpan;
  import android.util.AttributeSet;
  import android.view.LayoutInflater;
  import android.view.View;
  import android.view.ViewGroup;
  import android.widget.TextView;
  
  import java.text.BreakIterator;
  
  /**
   * This view is used to display text that will be too long to fit in the available space. The
   * text becomes focusable in the navigation and clicking on the text will open a dialog that
   * allows the user to read the full text.
   */
  @SuppressLint("WrongCall")
  //suppressing to remove suspicious method call warning for using 'onMeasure' instead of measure
  public class EllipsizedTextView extends TextView {
  
      private static final String TAG = EllipsizedTextView.class.getSimpleName();
      private static final char ELLIPSIS = '\u2026';
      private String mSetText;
      private boolean mIsEllipsized = false;
      private StateImageSpan mEllipsisImage;
      private CharSequence mCharSequence;
      private int mGuillemetDrawableId;
      private int mReadDialogWidth;
      private int mReadDialogHeight;
  
      private SingleViewProvider mExpandedContentViewProvider;
  
      /**
       * {@inheritDoc}
       */
      public EllipsizedTextView(final Context context) {
  
          super(context);
          mGuillemetDrawableId = R.drawable.guillemet;
      }
  
      /**
       * {@inheritDoc}
       */
      public EllipsizedTextView(final Context context, final AttributeSet attrs) {
  
          super(context, attrs);
          init(attrs);
      }
  
      /**
       * {@inheritDoc}
       */
      public EllipsizedTextView(final Context context, final AttributeSet attrs, final int defStyle) {
  
          super(context, attrs, defStyle);
          init(attrs);
      }
  
      /**
       * Initializes the view's attributes.
       *
       * @param attrs The attribute set.
       */
      private void init(final AttributeSet attrs) {
          // Load the layout parameters from attrs.
          final TypedArray styledAttributes = getContext().obtainStyledAttributes(attrs, R
                  .styleable.EllipsizedTextView);
  
          mGuillemetDrawableId =
                  styledAttributes.getResourceId(R.styleable.EllipsizedTextView_guillemetDrawable,
                                                 R.drawable.guillemet);
  
          mReadDialogWidth = (int) styledAttributes.getDimension(
                  R.styleable.EllipsizedTextView_readDialogWidth, 0);
  
          mReadDialogHeight = (int) styledAttributes.getDimension(
                  R.styleable.EllipsizedTextView_readDialogHeight, 0);
  
          styledAttributes.recycle();
      }
  
      /**
       * Creates the read dialog fragment and adds it to the fragment.
       */
      private void showReadDialog() {
          // Show the dialog
          final ReadDialogFragment dialog = new ReadDialogFragment();
          if (mExpandedContentViewProvider == null) {
              dialog.setContentViewProvider(getDefaultExpandedContentProvider(this));
          }
          else {
              dialog.setContentViewProvider(mExpandedContentViewProvider);
          }
  
          if (mReadDialogHeight != 0 && mReadDialogWidth != 0) {
              final Bundle args = new Bundle();
              args.putInt(ReadDialogFragment.INTENT_EXTRA_DIALOG_WIDTH, mReadDialogWidth);
              args.putInt(ReadDialogFragment.INTENT_EXTRA_DIALOG_HEIGHT, mReadDialogHeight);
  
              dialog.setArguments(args);
          }
  
          // Commit allowing state loss, in case our activity is being destroyed we won't be in an
          // illegal state.
          final FragmentTransaction ft = ((Activity) getContext()).getFragmentManager()
                                                                  .beginTransaction();
          ft.disallowAddToBackStack();
          ft.add(dialog, "read text");
          ft.commitAllowingStateLoss();
      }
  
      /**
       * {@inheritDoc}
       */
      @Override
      protected void onFinishInflate() {
  
          super.onFinishInflate();
  
          setOnClickListener(new OnClickListener() {
              @Override
              public void onClick(final View v) {
  
                  showReadDialog();
              }
          });
  
          // Force a state change so the ellipsis image is in the right state.
          drawableStateChanged();
      }
  
      /**
       * Default implementation of a SingleViewProvider.
       */
      private static SingleViewProvider getDefaultExpandedContentProvider(final EllipsizedTextView
                                                                                  textView) {
  
          return new SingleViewProvider() {
  
              @Override
              public View getView(final Context context, final LayoutInflater inflater, final
              ViewGroup parent) {
  
                  final View result = inflater.inflate(R.layout.read_dialog_default_layout, parent);
                  final TextView mainText = (TextView) result.findViewById(R.id.txt);
                  mainText.setText(textView.mSetText);
                  return result;
              }
          };
      }
  
      /**
       * {@inheritDoc}
       */
      @Override
      protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
  
          reflow(widthMeasureSpec, heightMeasureSpec);
      }
  
      /**
       * This function accomplishes two goals
       * <p>
       * # Measure all text using a staticLayout to determine where the text should be ellipsized
       * # Moves the ellipsis back one word to create more space for the image
       * # Replaces the ellipsis with a guillemet image that changes on focus
       * # Measures/Renders only the text visible
       * <p>
       * This function is slow and should only be called when needed
       * <p>
       * There is a bug in android that doesn't allow ellipsis to appear in multiline text fields
       * when the text is specified by Spannable.
       * <p>
       * To get around this we:
       * # Force this to use just text
       * # Layout twice once with just text, the second time with a Spannable with an image for an
       * ellipse
       */
      private void reflow(final int widthMeasureSpec, final int heightMeasureSpec) {
          // First layout
          super.setText(mCharSequence, BufferType.SPANNABLE);
          super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  
          final Layout layout = getLayout();
          if (layout == null) {
              // This will occur if the text is empty
              return;
          }
  
          final int lastLine = layout.getLineForOffset(mSetText.length());
          final int maxLines = getMaxLines();
  
          // Check if text should have an ellipsis.
          if (lastLine >= maxLines) {
              setEllipsis(widthMeasureSpec, heightMeasureSpec, layout, lastLine, maxLines);
          }
          else {
              // Reapply spans.
              if (mCharSequence instanceof Spannable) {
                  final Spannable span = (Spannable) mCharSequence;
                  super.setText(span, BufferType.SPANNABLE);
                  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
              }
              // No ellipsis, make this not focusable.
              removeEllipsisProperties();
          }
      }
  
      /**
       * Removes ellipsis properties.
       */
      private void removeEllipsisProperties() {
  
          mIsEllipsized = false;
          setFocusable(false);
          setClickable(false);
          mEllipsisImage = null;
      }
  
      /**
       * Sets ellipsis properties.
       *
       * @param widthMeasureSpec  Ellipsis width.
       * @param heightMeasureSpec Ellipsis height.
       * @param layout            Layout for ellipsis.
       * @param lastLine          Last line length for ellipsis.
       * @param maxLines          Max lines in ellipsis.
       */
      private void setEllipsis(int widthMeasureSpec, int heightMeasureSpec, Layout layout,
                               int lastLine, int maxLines) {
  
          mIsEllipsized = true;
          setFocusable(true);
          setClickable(true);
          final SpannableString ss = new SpannableString(mCharSequence);
          String visibleText = mCharSequence.toString();
  
  
          mEllipsisImage = new StateImageSpan(
                  getContext(),
                  mGuillemetDrawableId,
                  ImageSpan.ALIGN_BASELINE);
  
  
          final SpannableStringBuilder spannedText = new SpannableStringBuilder();
          int ellipsisIndex = layout.getLineStart(Math.min(lastLine + 1, maxLines));
  
          // Keep chopping words off until the ellipsis is on a visible line or there is only one
          // line left.
          do {
              // Only truncate the last line for long description.
              if (lastLine >= maxLines) {
                  // Getting the first word break index before the last index of maxline.
                  int safeBreakIndex = breakBefore(visibleText, ellipsisIndex, BreakIterator
                          .getWordInstance());
                  final int maxLineStart = layout.getLineStart(maxLines - 1);
  
                  // If this check pass, it means we just truncated a word that is longer than a line.
                  if (safeBreakIndex < maxLineStart) {
                      // Need to check character by character and break in the middle now. Checking
                      // word by word should cover most cases, only do this if a word is longer than
                      // line width.
                      safeBreakIndex = breakBefore(visibleText, ellipsisIndex,
                                                   BreakIterator.getCharacterInstance());
                  }
                  ellipsisIndex = safeBreakIndex;
              }
  
              visibleText = visibleText.substring(0, ellipsisIndex);
              final CharSequence charOutput = ss.subSequence(0, ellipsisIndex);
  
              // Re-add ellipsis and convert to image
              spannedText.replace(0, spannedText.length(), charOutput);
              spannedText.append(ELLIPSIS);
  
              spannedText.setSpan(mEllipsisImage, ellipsisIndex, ellipsisIndex + 1,
                                  Spanned.SPAN_INCLUSIVE_INCLUSIVE);
  
  
              // Reset text and re-measure.
              super.setText(spannedText, BufferType.SPANNABLE);
              super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  
          } while (getLineCount() > getMaxLines() && getLineCount() > 1);
          requestFocus();
      }
  
      /**
       * Find the first word/character break index before ellipsisIndex.
       */
      private int breakBefore(final String displayText, final int ellipsisIndex,
                              final BreakIterator iterator) {
  
          iterator.setText(displayText);
          return iterator.preceding(ellipsisIndex);
      }
  
  
      /**
       * Sets the text please note that text must be a normal CharSequence.
       * The text will be truncated if goes beyond max_characters_allowed characters
       */
      @Override
      public void setText(final CharSequence text, final BufferType type) {
  
          int maxCharactersAllowed = getContext()
                  .getResources().getInteger(R.integer.ellipsized_text_view_max_characters);
  
          if (text.length() > maxCharactersAllowed) {
              mCharSequence = text.subSequence(0, maxCharactersAllowed);
          }
          else {
              mCharSequence = text;
          }
          mSetText = mCharSequence.toString();
          super.setText(mCharSequence, type);
      }
  
      @Override
      protected void drawableStateChanged() {
  
          super.drawableStateChanged();
  
          // Notify the ellipsis of the state change so it can redraw.
          if (mEllipsisImage != null) {
              final int[] state = getDrawableState();
              mEllipsisImage.setState(state);
              invalidate();
          }
      }
  
      /**
       * This class is used to draw the guillemet at the end of the text instead of the ellipsis.
       * <p>
       * This code is a near duplicate of ImageSpan with the addition of a function setState that
       * sets the guillemet into the focus/unfocused state and lots of unneeded code to handle other
       * constructors removed.
       * <p>
       * Currently there enough edge cases that I am not inclined to make this a public class (i.e.
       * how the system will respond if the states of the images are different sizes).
       */
      private static class StateImageSpan extends DynamicDrawableSpan {
  
          private final StateListDrawable mStateDrawable;
  
          public StateImageSpan(final Context context, final int resourceId, final int
                  verticalAlignment) {
  
              super(verticalAlignment);
              mStateDrawable = (StateListDrawable) ContextCompat.getDrawable(context, resourceId);
              if (mStateDrawable != null) {
                  mStateDrawable.setBounds(0, 0, mStateDrawable.getIntrinsicWidth(), mStateDrawable
                          .getIntrinsicHeight());
                  mStateDrawable.setState(FOCUSED_STATE_SET);
              }
          }
  
          public void setState(final int[] state) {
  
              if (mStateDrawable != null) {
                  mStateDrawable.setState(state);
              }
          }
  
          @Override
          public Drawable getDrawable() {
  
              return mStateDrawable;
          }
      }
  }
  