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