1/*
2 * Copyright (C) 2017 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 androidx.emoji.widget;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.inputmethodservice.InputMethodService;
22import android.os.Build;
23import android.text.InputType;
24import android.util.AttributeSet;
25import android.view.LayoutInflater;
26import android.view.View;
27import android.view.ViewGroup;
28import android.view.inputmethod.EditorInfo;
29import android.view.inputmethod.InputConnection;
30import android.widget.LinearLayout;
31
32import androidx.annotation.NonNull;
33import androidx.annotation.Nullable;
34import androidx.annotation.RequiresApi;
35import androidx.emoji.R;
36import androidx.emoji.text.EmojiCompat;
37import androidx.emoji.text.EmojiSpan;
38
39/**
40 * Layout that contains emoji compatibility enhanced ExtractEditText. Should be used by
41 * {@link InputMethodService} implementations.
42 * <p/>
43 * Call {@link #onUpdateExtractingViews(InputMethodService, EditorInfo)} from
44 * {@link InputMethodService#onUpdateExtractingViews(EditorInfo)
45 * InputMethodService#onUpdateExtractingViews(EditorInfo)}.
46 * <pre>
47 * public class MyInputMethodService extends InputMethodService {
48 *     // ..
49 *     {@literal @}Override
50 *     public View onCreateExtractTextView() {
51 *         mExtractView = getLayoutInflater().inflate(R.layout.emoji_input_method_extract_layout,
52 *                 null);
53 *         return mExtractView;
54 *     }
55 *
56 *     {@literal @}Override
57 *     public void onUpdateExtractingViews(EditorInfo ei) {
58 *         mExtractView.onUpdateExtractingViews(this, ei);
59 *     }
60 * }
61 * </pre>
62 *
63 * @attr ref androidx.emoji.R.styleable#EmojiExtractTextLayout_emojiReplaceStrategy
64 */
65public class EmojiExtractTextLayout extends LinearLayout {
66
67    private ExtractButtonCompat mExtractAction;
68    private EmojiExtractEditText mExtractEditText;
69    private ViewGroup mExtractAccessories;
70    private View.OnClickListener mButtonOnClickListener;
71
72    /**
73     * Prevent calling {@link #init(Context, AttributeSet, int)}} multiple times in case super()
74     * constructors call other constructors.
75     */
76    private boolean mInitialized;
77
78    public EmojiExtractTextLayout(Context context) {
79        super(context);
80        init(context, null /*attrs*/, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
81    }
82
83    public EmojiExtractTextLayout(Context context,
84            @Nullable AttributeSet attrs) {
85        super(context, attrs);
86        init(context, attrs, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
87    }
88
89    public EmojiExtractTextLayout(Context context,
90            @Nullable AttributeSet attrs, int defStyleAttr) {
91        super(context, attrs, defStyleAttr);
92        init(context, attrs, defStyleAttr, 0 /*defStyleRes*/);
93    }
94
95    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
96    public EmojiExtractTextLayout(Context context, AttributeSet attrs,
97            int defStyleAttr, int defStyleRes) {
98        super(context, attrs, defStyleAttr, defStyleRes);
99        init(context, attrs, defStyleAttr, defStyleRes);
100    }
101
102    private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
103            int defStyleRes) {
104        if (!mInitialized) {
105            mInitialized = true;
106            setOrientation(HORIZONTAL);
107            final View view = LayoutInflater.from(context)
108                    .inflate(R.layout.input_method_extract_view, this /*root*/,
109                            true /*attachToRoot*/);
110            mExtractAccessories = view.findViewById(R.id.inputExtractAccessories);
111            mExtractAction = view.findViewById(R.id.inputExtractAction);
112            mExtractEditText = view.findViewById(android.R.id.inputExtractEditText);
113
114            if (attrs != null) {
115                final TypedArray a = context.obtainStyledAttributes(attrs,
116                        R.styleable.EmojiExtractTextLayout, defStyleAttr, defStyleRes);
117                final int replaceStrategy = a.getInteger(
118                        R.styleable.EmojiExtractTextLayout_emojiReplaceStrategy,
119                        EmojiCompat.REPLACE_STRATEGY_DEFAULT);
120                mExtractEditText.setEmojiReplaceStrategy(replaceStrategy);
121                a.recycle();
122            }
123        }
124    }
125
126    /**
127     * Sets whether to replace all emoji with {@link EmojiSpan}s. Default value is
128     * {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT}.
129     *
130     * @param replaceStrategy should be one of {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT},
131     *                        {@link EmojiCompat#REPLACE_STRATEGY_NON_EXISTENT},
132     *                        {@link EmojiCompat#REPLACE_STRATEGY_ALL}
133     *
134     * @attr ref androidx.emoji.R.styleable#EmojiExtractTextLayout_emojiReplaceStrategy
135     */
136    public void setEmojiReplaceStrategy(@EmojiCompat.ReplaceStrategy int replaceStrategy) {
137        mExtractEditText.setEmojiReplaceStrategy(replaceStrategy);
138    }
139
140    /**
141     * Returns whether to replace all emoji with {@link EmojiSpan}s. Default value is
142     * {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT}.
143     *
144     * @return one of {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT},
145     *                        {@link EmojiCompat#REPLACE_STRATEGY_NON_EXISTENT},
146     *                        {@link EmojiCompat#REPLACE_STRATEGY_ALL}
147     *
148     * @attr ref androidx.emoji.R.styleable#EmojiExtractTextLayout_emojiReplaceStrategy
149     */
150    public int getEmojiReplaceStrategy() {
151        return mExtractEditText.getEmojiReplaceStrategy();
152    }
153
154    /**
155     * Initializes the layout. Call this function from
156     * {@link InputMethodService#onUpdateExtractingViews(EditorInfo)
157     * InputMethodService#onUpdateExtractingViews(EditorInfo)}.
158     */
159    public void onUpdateExtractingViews(InputMethodService inputMethodService, EditorInfo ei) {
160        // the following code is ported as it is from InputMethodService.onUpdateExtractingViews
161        if (!inputMethodService.isExtractViewShown()) {
162            return;
163        }
164
165        if (mExtractAccessories == null) {
166            return;
167        }
168
169        final boolean hasAction = ei.actionLabel != null
170                || ((ei.imeOptions & EditorInfo.IME_MASK_ACTION) != EditorInfo.IME_ACTION_NONE
171                && (ei.imeOptions & EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION) == 0
172                && ei.inputType != InputType.TYPE_NULL);
173
174        if (hasAction) {
175            mExtractAccessories.setVisibility(View.VISIBLE);
176            if (mExtractAction != null) {
177                if (ei.actionLabel != null) {
178                    mExtractAction.setText(ei.actionLabel);
179                } else {
180                    mExtractAction.setText(inputMethodService.getTextForImeAction(ei.imeOptions));
181                }
182                mExtractAction.setOnClickListener(getButtonClickListener(inputMethodService));
183            }
184        } else {
185            mExtractAccessories.setVisibility(View.GONE);
186            if (mExtractAction != null) {
187                mExtractAction.setOnClickListener(null);
188            }
189        }
190    }
191
192    private View.OnClickListener getButtonClickListener(
193            final InputMethodService inputMethodService) {
194        if (mButtonOnClickListener == null) {
195            mButtonOnClickListener = new ButtonOnclickListener(inputMethodService);
196        }
197        return mButtonOnClickListener;
198    }
199
200    private static final class ButtonOnclickListener implements View.OnClickListener {
201        private final InputMethodService mInputMethodService;
202
203        ButtonOnclickListener(InputMethodService inputMethodService) {
204            mInputMethodService = inputMethodService;
205        }
206
207        /**
208         * The following code is ported as it is from InputMethodService.mActionClickListener.
209         */
210        @Override
211        public void onClick(View v) {
212            final EditorInfo ei = mInputMethodService.getCurrentInputEditorInfo();
213            final InputConnection ic = mInputMethodService.getCurrentInputConnection();
214            if (ei != null && ic != null) {
215                if (ei.actionId != 0) {
216                    ic.performEditorAction(ei.actionId);
217                } else if ((ei.imeOptions & EditorInfo.IME_MASK_ACTION)
218                        != EditorInfo.IME_ACTION_NONE) {
219                    ic.performEditorAction(ei.imeOptions & EditorInfo.IME_MASK_ACTION);
220                }
221            }
222        }
223    }
224}
225