RecipientEditTextView.java revision 55bb2833b29945c08b809408ff94ddf7703e911a
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.ex.chips;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.graphics.Rect;
24import android.graphics.drawable.BitmapDrawable;
25import android.graphics.drawable.Drawable;
26import android.text.Editable;
27import android.text.Layout;
28import android.text.Spannable;
29import android.text.SpannableString;
30import android.text.Spanned;
31import android.text.TextPaint;
32import android.text.TextUtils;
33import android.text.TextWatcher;
34import android.text.method.QwertyKeyListener;
35import android.text.style.ImageSpan;
36import android.util.AttributeSet;
37import android.util.Log;
38import android.view.ActionMode;
39import android.view.KeyEvent;
40import android.view.Menu;
41import android.view.MenuItem;
42import android.view.MotionEvent;
43import android.view.View;
44import android.view.ActionMode.Callback;
45import android.widget.AdapterView;
46import android.widget.AdapterView.OnItemClickListener;
47import android.widget.ListPopupWindow;
48import android.widget.MultiAutoCompleteTextView;
49
50import java.util.Collection;
51import java.util.HashSet;
52import java.util.Set;
53
54import java.util.ArrayList;
55
56/**
57 * RecipientEditTextView is an auto complete text view for use with applications
58 * that use the new Chips UI for addressing a message to recipients.
59 */
60public class RecipientEditTextView extends MultiAutoCompleteTextView
61    implements OnItemClickListener, Callback {
62
63    private static final String TAG = "RecipientEditTextView";
64
65    private Drawable mChipBackground = null;
66
67    private Drawable mChipDelete = null;
68
69    private int mChipPadding;
70
71    private Tokenizer mTokenizer;
72
73    private Drawable mChipBackgroundPressed;
74
75    private RecipientChip mSelectedChip;
76
77    private int mChipDeleteWidth;
78
79    private ArrayList<RecipientChip> mRecipients;
80
81    private int mAlternatesLayout;
82
83    private int mAlternatesSelectedLayout;
84
85    public RecipientEditTextView(Context context, AttributeSet attrs) {
86        super(context, attrs);
87        mRecipients = new ArrayList<RecipientChip>();
88        setOnItemClickListener(this);
89        setCustomSelectionActionModeCallback(this);
90        // When the user starts typing, make sure we unselect any selected
91        // chips.
92        addTextChangedListener(new TextWatcher() {
93            @Override
94            public void afterTextChanged(Editable s) {
95                // Do nothing.
96            }
97            @Override
98            public void onTextChanged(CharSequence s, int start, int before, int count) {
99                // Do nothing.
100            }
101            @Override
102            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
103                if (mSelectedChip != null) {
104                    clearSelectedChip();
105                    setSelection(getText().length());
106                }
107            }
108        });
109    }
110
111    @Override
112    public void onSelectionChanged(int start, int end) {
113        // When selection changes, see if it is inside the chips area.
114        // If so, move the cursor back after the chips again.
115        if (mRecipients != null && mRecipients.size() > 0) {
116            Spannable span = getSpannable();
117            RecipientChip[] chips = span.getSpans(start, getText().length(), RecipientChip.class);
118            if (chips != null && chips.length > 0) {
119                // Grab the last chip and set the cursor to after it.
120                setSelection(chips[chips.length - 1].getChipEnd() + 1);
121            }
122        } else {
123            super.onSelectionChanged(start, end);
124        }
125    }
126
127    public RecipientChip constructChipSpan(RecipientEntry contact, int offset, boolean pressed)
128        throws NullPointerException {
129        if (mChipBackground == null) {
130            throw new NullPointerException
131                ("Unable to render any chips as setChipDimensions was not called.");
132        }
133        String text = contact.getDisplayName();
134        Layout layout = getLayout();
135        int line = layout.getLineForOffset(offset);
136        int lineTop = layout.getLineTop(line);
137
138        TextPaint paint = getPaint();
139        float defaultSize = paint.getTextSize();
140
141        // Reduce the size of the text slightly so that we can get the "look" of
142        // padding.
143        paint.setTextSize((float) (paint.getTextSize() * .9));
144
145        // Ellipsize the text so that it takes AT MOST the entire width of the
146        // autocomplete text entry area. Make sure to leave space for padding
147        // on the sides.
148        CharSequence ellipsizedText = TextUtils.ellipsize(text, paint,
149                calculateAvailableWidth(pressed), TextUtils.TruncateAt.END);
150
151        int height = getLineHeight();
152        int width = (int) Math.floor(paint.measureText(ellipsizedText, 0, ellipsizedText.length()))
153                + (mChipPadding * 2);
154        if (pressed) {
155            // Allow the delete icon to overtake the visible recipient name.
156            // This works since when the user has entered selected mode, they
157            // will also see a popup with the recipient name.
158            width += mChipDeleteWidth/2;
159        }
160
161        // Create the background of the chip.
162        Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
163        Canvas canvas = new Canvas(tmpBitmap);
164        if (pressed) {
165            if (mChipBackgroundPressed != null) {
166                mChipBackgroundPressed.setBounds(0, 0, width, height);
167                mChipBackgroundPressed.draw(canvas);
168
169                // Align the display text with where the user enters text.
170                canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding, height
171                        - layout.getLineDescent(line), paint);
172                mChipDelete.setBounds(width - mChipDeleteWidth, 0, width, height);
173                mChipDelete.draw(canvas);
174            } else {
175                Log.w(TAG,
176                        "Unable to draw a background for the chips as it was never set");
177            }
178        } else {
179            if (mChipBackground != null) {
180                mChipBackground.setBounds(0, 0, width, height);
181                mChipBackground.draw(canvas);
182
183                // Align the display text with where the user enters text.
184                canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding, height
185                        - layout.getLineDescent(line), paint);
186            } else {
187                Log.w(TAG,
188                        "Unable to draw a background for the chips as it was never set");
189            }
190        }
191
192
193        // Get the location of the widget so we can properly offset
194        // the anchor for each chip.
195        int[] xy = new int[2];
196        getLocationOnScreen(xy);
197        // Pass the full text, un-ellipsized, to the chip.
198        Drawable result = new BitmapDrawable(getResources(), tmpBitmap);
199        result.setBounds(0, 0, width, height);
200        Rect bounds = new Rect(xy[0] + offset, xy[1] + lineTop, xy[0] + width,
201                calculateLineBottom(xy[1], line));
202        RecipientChip recipientChip = new RecipientChip(result, contact, offset, bounds);
203
204        // Return text to the original size.
205        paint.setTextSize(defaultSize);
206
207        return recipientChip;
208    }
209
210    // The bottom of the line the chip will be located on is calculated by 4 factors:
211    // 1) which line the chip appears on
212    // 2) the height of a line in the autocomplete view
213    // 3) padding built into the edit text view will move the bottom position
214    // 4) the position of the autocomplete view on the screen, taking into account
215    // that any top padding will move this down visually
216    private int calculateLineBottom(int yOffset, int line) {
217        int bottomPadding = 0;
218        if (line == getLineCount() - 1) {
219            bottomPadding += getPaddingBottom();
220        }
221        return ((line + 1) * getLineHeight()) + (yOffset + getPaddingTop()) + bottomPadding;
222    }
223
224    // Get the max amount of space a chip can take up. The formula takes into
225    // account the width of the EditTextView, any view padding, and padding
226    // that will be added to the chip.
227    private float calculateAvailableWidth(boolean pressed) {
228        int paddingRight = 0;
229        if (pressed) {
230            paddingRight = mChipDeleteWidth;
231        }
232        return getWidth() - getPaddingLeft() - getPaddingRight() - (mChipPadding * 2)
233                - paddingRight;
234    }
235
236    /**
237     * Set all chip dimensions and resources. This has to be done from the application
238     * as this is a static library.
239     * @param chipBackground drawable
240     * @param padding Padding around the text in a chip
241     * @param offset Offset between the chip and the dropdown of alternates
242     */
243    public void setChipDimensions(Drawable chipBackground, Drawable chipBackgroundPressed,
244            Drawable chipDelete, int alternatesLayout, int alternatesSelectedLayout, float padding) {
245        mChipBackground = chipBackground;
246        mChipBackgroundPressed = chipBackgroundPressed;
247        mChipDelete = chipDelete;
248        mChipDeleteWidth = chipDelete.getIntrinsicWidth();
249        mChipPadding = (int) padding;
250        mAlternatesLayout = alternatesLayout;
251        mAlternatesSelectedLayout = alternatesSelectedLayout;
252    }
253
254    @Override
255    public void setTokenizer(Tokenizer tokenizer) {
256        mTokenizer = tokenizer;
257        super.setTokenizer(mTokenizer);
258    }
259
260    // We want to handle replacing text in the onItemClickListener
261    // so we can get all the associated contact information including
262    // display text, address, and id.
263    @Override
264    protected void replaceText(CharSequence text) {
265        return;
266    }
267
268    @Override
269    public boolean onKeyUp(int keyCode, KeyEvent event) {
270        switch (keyCode) {
271            case KeyEvent.KEYCODE_ENTER:
272            case KeyEvent.KEYCODE_DPAD_CENTER:
273            case KeyEvent.KEYCODE_TAB:
274                if (event.hasNoModifiers()) {
275                    if (isPopupShowing()) {
276                        // choose the first entry.
277                        submitItemAtPosition(0);
278                        dismissDropDown();
279                        return true;
280                    } else {
281                        int end = getSelectionEnd();
282                        int start = mTokenizer.findTokenStart(getText(), end);
283                        String text = getText().toString().substring(start, end);
284                        clearComposingText();
285
286                        Editable editable = getText();
287                        RecipientEntry entry = RecipientEntry.constructFakeEntry(text);
288                        QwertyKeyListener.markAsReplaced(editable, start, end, "");
289                        editable.replace(start, end, createChip(entry));
290                        dismissDropDown();
291                    }
292                }
293        }
294        return super.onKeyUp(keyCode, event);
295    }
296
297    @Override
298    public boolean onKeyDown(int keyCode, KeyEvent event) {
299        if (mSelectedChip != null) {
300            mSelectedChip.onKeyDown(keyCode, event);
301        }
302
303        if (keyCode == KeyEvent.KEYCODE_ENTER && event.hasNoModifiers()) {
304            return true;
305        }
306
307        return super.onKeyDown(keyCode, event);
308    }
309
310    private Spannable getSpannable() {
311        return (Spannable) getText();
312    }
313
314    /**
315     * Instead of filtering on the entire contents of the edit box,
316     * this subclass method filters on the range from
317     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
318     * if the length of that range meets or exceeds {@link #getThreshold}
319     * and makes sure that the range is not already a Chip.
320     */
321    @Override
322    protected void performFiltering(CharSequence text, int keyCode) {
323        if (enoughToFilter()) {
324            int end = getSelectionEnd();
325            int start = mTokenizer.findTokenStart(text, end);
326            // If this is a RecipientChip, don't filter
327            // on its contents.
328            Spannable span = getSpannable();
329            RecipientChip[] chips = span.getSpans(start, end, RecipientChip.class);
330            if (chips != null && chips.length > 0) {
331                return;
332            }
333        }
334        super.performFiltering(text, keyCode);
335    }
336
337    private void clearSelectedChip() {
338        if (mSelectedChip != null) {
339            mSelectedChip.unselectChip();
340            mSelectedChip = null;
341        }
342    }
343
344    @Override
345    public boolean onTouchEvent(MotionEvent event) {
346        int action = event.getAction();
347        boolean handled = super.onTouchEvent(event);
348        boolean chipWasSelected = false;
349
350        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
351            float x = event.getX();
352            float y = event.getY();
353            int offset = putOffsetInRange(getOffsetForPosition(x, y));
354            RecipientChip currentChip = findChip(offset);
355            if (currentChip != null) {
356                if (action == MotionEvent.ACTION_UP) {
357                    if (mSelectedChip != null && mSelectedChip != currentChip) {
358                        clearSelectedChip();
359                        mSelectedChip = currentChip.selectChip();
360                    } else if (mSelectedChip == null) {
361                        mSelectedChip = currentChip.selectChip();
362                    } else {
363                        mSelectedChip.onClick(this, offset, x, y);
364                    }
365                }
366                chipWasSelected = true;
367            }
368        }
369        if (action == MotionEvent.ACTION_UP && !chipWasSelected) {
370            clearSelectedChip();
371        }
372        return handled;
373    }
374
375    // TODO: This algorithm will need a lot of tweaking after more people have used
376    // the chips ui. This attempts to be "forgiving" to fat finger touches by favoring
377    // what comes before the finger.
378    private int putOffsetInRange(int o) {
379        int offset = o;
380        Editable text = getText();
381        int length = text.length();
382        // Remove whitespace from end to find "real end"
383        int realLength = length;
384        for (int i = length - 1; i >= 0; i--) {
385            if (text.charAt(i) == ' ') {
386                realLength--;
387            } else {
388                break;
389            }
390        }
391
392        // If the offset is beyond or at the end of the text,
393        // leave it alone.
394        if (offset >= realLength) {
395            return offset;
396        }
397        Editable editable = getText();
398        while (offset >= 0 && findText(editable, offset) == -1 && findChip(offset) == null) {
399            // Keep walking backward!
400            offset--;
401        }
402        return offset;
403    }
404
405    private int findText(Editable text, int offset) {
406        if (text.charAt(offset) != ' ') {
407            return offset;
408        }
409        return -1;
410    }
411
412    private RecipientChip findChip(int offset) {
413        RecipientChip[] chips = getSpannable().getSpans(0, getText().length(), RecipientChip.class);
414        // Find the chip that contains this offset.
415        for (int i = 0; i < chips.length; i++) {
416            RecipientChip chip = chips[i];
417            if (chip.matchesChip(offset)) {
418                return chip;
419            }
420        }
421        return null;
422    }
423
424    private CharSequence createChip(RecipientEntry entry) {
425        CharSequence displayText = mTokenizer.terminateToken(entry.getDestination());
426        // Always leave a blank space at the end of a chip.
427        int textLength = displayText.length();
428        if (displayText.charAt(textLength - 1) == ' ') {
429            textLength--;
430        } else {
431            displayText = displayText.toString().concat(" ");
432            textLength = displayText.length();
433        }
434        SpannableString chipText = new SpannableString(displayText);
435        int end = getSelectionEnd();
436        int start = mTokenizer.findTokenStart(getText(), end);
437        try {
438            chipText.setSpan(constructChipSpan(entry, start, false), 0, textLength,
439                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
440        } catch (NullPointerException e) {
441            Log.e(TAG, e.getMessage(), e);
442            return null;
443        }
444
445        return chipText;
446    }
447
448    @Override
449    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
450        submitItemAtPosition(position);
451    }
452
453    private void submitItemAtPosition(int position) {
454        RecipientEntry entry = (RecipientEntry) getAdapter().getItem(position);
455        clearComposingText();
456
457        int end = getSelectionEnd();
458        int start = mTokenizer.findTokenStart(getText(), end);
459
460        Editable editable = getText();
461        editable.replace(start, end, createChip(entry));
462        QwertyKeyListener.markAsReplaced(editable, start, end, "");
463    }
464
465    /** Returns a collection of contact Id for each chip inside this View. */
466    /* package */ Collection<Long> getContactIds() {
467        final Set<Long> result = new HashSet<Long>();
468        for (RecipientChip chip : mRecipients) {
469            result.add(chip.getContactId());
470        }
471        return result;
472    }
473
474    /** Returns a collection of data Id for each chip inside this View. May be null. */
475    /* package */ Collection<Long> getDataIds() {
476        final Set<Long> result = new HashSet<Long>();
477        for (RecipientChip chip : mRecipients) {
478            result.add(chip.getDataId());
479        }
480        return result;
481    }
482
483    /**
484     * RecipientChip defines an ImageSpan that contains information relevant to
485     * a particular recipient.
486     */
487    public class RecipientChip extends ImageSpan implements OnItemClickListener {
488        private final CharSequence mDisplay;
489
490        private final CharSequence mValue;
491
492        private final int mOffset;
493
494        private ListPopupWindow mPopup;
495
496        private View mAnchorView;
497
498        private int mLeft;
499
500        private final long mContactId;
501
502        private final long mDataId;
503
504        private RecipientEntry mEntry;
505
506        private boolean mSelected = false;
507
508        private RecipientAlternatesAdapter mAlternatesAdapter;
509
510        private Rect mBounds;
511
512        public RecipientChip(Drawable drawable, RecipientEntry entry, int offset, Rect bounds) {
513            super(drawable);
514            mDisplay = entry.getDisplayName();
515            mValue = entry.getDestination();
516            mContactId = entry.getContactId();
517            mDataId = entry.getDataId();
518            mOffset = offset;
519            mEntry = entry;
520            mBounds = bounds;
521
522            mAnchorView = new View(getContext());
523            mAnchorView.setLeft(bounds.left);
524            mAnchorView.setRight(bounds.left);
525            mAnchorView.setTop(bounds.bottom);
526            mAnchorView.setBottom(bounds.bottom);
527            mAnchorView.setVisibility(View.GONE);
528            mRecipients.add(this);
529        }
530
531        public void unselectChip() {
532            if (getChipStart() == -1 || getChipEnd() == -1) {
533                mSelectedChip = null;
534                return;
535            }
536            clearComposingText();
537            RecipientChip newChipSpan = null;
538            try {
539                newChipSpan = constructChipSpan(mEntry, mOffset, false);
540            } catch (NullPointerException e) {
541                Log.e(TAG, e.getMessage(), e);
542                return;
543            }
544            replace(newChipSpan);
545            if (mPopup != null && mPopup.isShowing()) {
546                mPopup.dismiss();
547            }
548            return;
549        }
550
551        public void onKeyDown(int keyCode, KeyEvent event) {
552            if (keyCode == KeyEvent.KEYCODE_DEL) {
553                if (mPopup != null && mPopup.isShowing()) {
554                    mPopup.dismiss();
555                }
556                removeChip();
557            }
558        }
559
560        public boolean isCompletedContact() {
561            return mContactId != -1;
562        }
563
564        private void replace(RecipientChip newChip) {
565            Spannable spannable = getSpannable();
566            int spanStart = getChipStart();
567            int spanEnd = getChipEnd();
568            QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
569            spannable.removeSpan(this);
570            mRecipients.remove(this);
571            spannable.setSpan(newChip, spanStart, spanEnd, 0);
572        }
573
574        public void removeChip() {
575            Spannable spannable = getSpannable();
576            int spanStart = getChipStart();
577            int spanEnd = getChipEnd();
578            if (this == mSelectedChip) {
579                mSelectedChip = null;
580            }
581            Editable text = getText();
582            int toDelete = spanEnd;
583            // Always remove trailing spaces when removing a chip.
584            while (toDelete >= 0 && toDelete < text.length() - 1 && text.charAt(toDelete) == ' ') {
585                toDelete++;
586            }
587            QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
588            spannable.removeSpan(this);
589            mRecipients.remove(this);
590            spannable.setSpan(null, spanStart, spanEnd, 0);
591            text.delete(spanStart, toDelete);
592        }
593
594        public int getChipStart() {
595            return getSpannable().getSpanStart(this);
596        }
597
598        public int getChipEnd() {
599            return getSpannable().getSpanEnd(this);
600        }
601
602        public void replaceChip(RecipientEntry entry) {
603            clearComposingText();
604
605            RecipientChip newChipSpan = null;
606            try {
607                newChipSpan = constructChipSpan(entry, mOffset, false);
608            } catch (NullPointerException e) {
609                Log.e(TAG, e.getMessage(), e);
610                return;
611            }
612            replace(newChipSpan);
613            if (mPopup != null && mPopup.isShowing()) {
614                mPopup.dismiss();
615            }
616        }
617
618        public RecipientChip selectChip() {
619            clearComposingText();
620            RecipientChip newChipSpan = null;
621            if (isCompletedContact()) {
622                try {
623                    newChipSpan = constructChipSpan(mEntry, mOffset, true);
624                    newChipSpan.setSelected(true);
625                } catch (NullPointerException e) {
626                    Log.e(TAG, e.getMessage(), e);
627                    return newChipSpan;
628                }
629                replace(newChipSpan);
630                if (mPopup != null && mPopup.isShowing()) {
631                    mPopup.dismiss();
632                }
633                mSelected = true;
634                // Make sure we call edit on the new chip span.
635                newChipSpan.showAlternates();
636            } else {
637                CharSequence text = getValue();
638                removeChip();
639                Editable editable = getText();
640                setSelection(editable.length());
641                editable.append(text);
642            }
643            return newChipSpan;
644        }
645
646        private void showAlternates() {
647            mPopup = new ListPopupWindow(RecipientEditTextView.this.getContext());
648
649            if (!mPopup.isShowing()) {
650                mAlternatesAdapter = new RecipientAlternatesAdapter(
651                        RecipientEditTextView.this.getContext(),
652                        mEntry.getContactId(), mEntry.getDataId(),
653                        mAlternatesLayout, mAlternatesSelectedLayout);
654                mAnchorView.setLeft(mLeft);
655                mAnchorView.setRight(mLeft);
656                mPopup.setAnchorView(mAnchorView);
657                mPopup.setAdapter(mAlternatesAdapter);
658                mPopup.setWidth(getWidth());
659                mPopup.setOnItemClickListener(this);
660                mPopup.show();
661            }
662        }
663
664        private void setSelected(boolean selected) {
665            mSelected = selected;
666        }
667
668        public CharSequence getDisplay() {
669            return mDisplay;
670        }
671
672        public CharSequence getValue() {
673            return mValue;
674        }
675
676        private boolean isInDelete(int offset, float x, float y) {
677            // Figure out the bounds of this chip and whether or not
678            // the user clicked in the X portion.
679            return mSelected
680                    && (offset == getChipEnd()
681                            || (x > (mBounds.right - mChipDeleteWidth) && x < mBounds.right));
682        }
683
684        public boolean matchesChip(int offset) {
685            int start = getChipStart();
686            int end = getChipEnd();
687            return (offset >= start && offset <= end);
688        }
689
690        public void onClick(View widget, int offset, float x, float y) {
691            if (mSelected) {
692                if (isInDelete(offset, x, y)) {
693                    removeChip();
694                    return;
695                }
696            }
697        }
698
699        @Override
700        public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top,
701                int y, int bottom, Paint paint) {
702            // Shift the bounds of this span to where it is actually drawn on the screeen.
703            mLeft = (int) x;
704            super.draw(canvas, text, start, end, x, top, y, bottom, paint);
705        }
706
707        @Override
708        public void onItemClick(AdapterView<?> adapterView, View view, int position, long rowId) {
709            mPopup.dismiss();
710            clearComposingText();
711            replaceChip(mAlternatesAdapter.getRecipientEntry(position));
712        }
713
714        public long getContactId() {
715            return mContactId;
716        }
717
718        public long getDataId() {
719            return mDataId;
720        }
721    }
722
723    @Override
724    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
725        return false;
726    }
727
728    @Override
729    public void onDestroyActionMode(ActionMode mode) {
730    }
731
732    @Override
733    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
734        return false;
735    }
736
737    // Prevent selection of chips.
738    @Override
739    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
740        return false;
741    }
742}
743
744