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