1 /*
2  * Copyright (C) 2022 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 com.android.settings.inputmethod;
18 
19 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
20 
21 import android.app.Activity;
22 import android.app.AlertDialog;
23 import android.app.Dialog;
24 import android.app.settings.SettingsEnums;
25 import android.content.Context;
26 import android.graphics.drawable.Drawable;
27 import android.hardware.input.InputManager;
28 import android.os.Bundle;
29 import android.text.Spannable;
30 import android.text.SpannableString;
31 import android.text.style.ForegroundColorSpan;
32 import android.view.KeyEvent;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.Window;
37 import android.widget.AdapterView;
38 import android.widget.BaseAdapter;
39 import android.widget.Button;
40 import android.widget.ImageView;
41 import android.widget.ListView;
42 import android.widget.TextView;
43 
44 import androidx.core.graphics.drawable.DrawableCompat;
45 import androidx.fragment.app.DialogFragment;
46 import androidx.preference.Preference;
47 
48 import com.android.settings.R;
49 import com.android.settings.overlay.FeatureFactory;
50 import com.android.settingslib.Utils;
51 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
52 
53 import java.util.ArrayList;
54 import java.util.Arrays;
55 import java.util.HashMap;
56 import java.util.List;
57 import java.util.Map;
58 
59 public class ModifierKeysPickerDialogFragment extends DialogFragment {
60 
61     static final String DEFAULT_KEY = "default_key";
62     static final String SELECTION_KEY = "delection_key";
63 
64     private Preference mPreference;
65     private String mKeyDefaultName;
66     private String mKeyFocus;
67     private Activity mActivity;
68     private KeyboardSettingsFeatureProvider mFeatureProvider;
69     private Drawable mActionKeyDrawable;
70     private TextView mLeftBracket;
71     private TextView mRightBracket;
72     private ImageView mActionKeyIcon;
73     private MetricsFeatureProvider mMetricsFeatureProvider;
74 
75     private List<int[]> mRemappableKeyList =
76             new ArrayList<>(Arrays.asList(
77                     new int[]{KeyEvent.KEYCODE_CAPS_LOCK},
78                     new int[]{KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT},
79                     new int[]{KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_META_RIGHT},
80                     new int[]{KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT}));
81 
82     private Map<String, int[]> mRemappableKeyMap = new HashMap<>();
83 
ModifierKeysPickerDialogFragment()84     public ModifierKeysPickerDialogFragment() {}
85 
86     @Override
onSaveInstanceState(Bundle savedInstanceState)87     public void onSaveInstanceState(Bundle savedInstanceState) {
88         savedInstanceState.putString(SELECTION_KEY, mKeyFocus);
89         super.onSaveInstanceState(savedInstanceState);
90     }
91 
92     @Override
onCreateDialog(Bundle savedInstanceState)93     public Dialog onCreateDialog(Bundle savedInstanceState) {
94         super.onCreateDialog(savedInstanceState);
95 
96         mActivity = getActivity();
97         FeatureFactory featureFactory = FeatureFactory.getFeatureFactory();
98         mMetricsFeatureProvider = featureFactory.getMetricsFeatureProvider();
99         mFeatureProvider = featureFactory.getKeyboardSettingsFeatureProvider();
100         InputManager inputManager = mActivity.getSystemService(InputManager.class);
101         mKeyDefaultName = getArguments().getString(DEFAULT_KEY);
102         mKeyFocus = getArguments().getString(SELECTION_KEY);
103         if (savedInstanceState != null) {
104             mKeyFocus = savedInstanceState.getString(SELECTION_KEY);
105         }
106         List<String> modifierKeys = new ArrayList<String>(Arrays.asList(
107                 mActivity.getString(R.string.modifier_keys_caps_lock),
108                 mActivity.getString(R.string.modifier_keys_ctrl),
109                 mActivity.getString(R.string.modifier_keys_meta),
110                 mActivity.getString(R.string.modifier_keys_alt)));
111         for (int i = 0; i < modifierKeys.size(); i++) {
112             mRemappableKeyMap.put(modifierKeys.get(i), mRemappableKeyList.get(i));
113         }
114         Drawable drawable = mFeatureProvider.getActionKeyIcon(mActivity);
115         if (drawable != null) {
116             mActionKeyDrawable = DrawableCompat.wrap(drawable);
117         }
118 
119         View dialoglayout  =
120                 LayoutInflater.from(mActivity).inflate(R.layout.modifier_key_picker_dialog, null);
121         AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mActivity);
122         dialogBuilder.setView(dialoglayout);
123 
124         TextView summary = dialoglayout.findViewById(R.id.modifier_key_picker_summary);
125         CharSequence summaryText = mActivity.getString(
126                 R.string.modifier_keys_picker_summary, mKeyDefaultName);
127         summary.setText(summaryText);
128 
129         ModifierKeyAdapter adapter = new ModifierKeyAdapter(modifierKeys);
130         ListView listView = dialoglayout.findViewById(R.id.modifier_key_picker);
131         listView.setAdapter(adapter);
132         setInitialFocusItem(modifierKeys, adapter);
133         listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
134             @Override
135             public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
136                 adapter.setCurrentItem(i);
137                 adapter.notifyDataSetChanged();
138             }
139         });
140 
141         AlertDialog modifierKeyDialog = dialogBuilder.create();
142         Button doneButton = dialoglayout.findViewById(R.id.modifier_key_done_button);
143         doneButton.setOnClickListener(v -> {
144             String selectedItem = modifierKeys.get(adapter.getCurrentItem());
145             Spannable itemSummary;
146             logMetricsForRemapping(selectedItem);
147             if (selectedItem.equals(mKeyDefaultName)) {
148                 itemSummary = new SpannableString(
149                         mActivity.getString(R.string.modifier_keys_default_summary));
150                 itemSummary.setSpan(
151                         new ForegroundColorSpan(getColorOfTextColorSecondary()),
152                         0, itemSummary.length(), 0);
153                 // Set keys to default.
154                 int[] keys = mRemappableKeyMap.get(mKeyDefaultName);
155                 for (int i = 0; i < keys.length; i++) {
156                     inputManager.remapModifierKey(keys[i], keys[i]);
157                 }
158             } else {
159                 itemSummary = new SpannableString(selectedItem);
160                 itemSummary.setSpan(
161                         new ForegroundColorSpan(getColorOfMaterialColorPrimary()),
162                         0, itemSummary.length(), 0);
163                 int[] fromKeys = mRemappableKeyMap.get(mKeyDefaultName);
164                 int[] toKeys = mRemappableKeyMap.get(selectedItem);
165                 // CAPS_LOCK only one key, so always choose the left key for remapping.
166                 if (isKeyCapsLock(mActivity, mKeyDefaultName)) {
167                     inputManager.remapModifierKey(fromKeys[0], toKeys[0]);
168                 }
169                 // Remap KEY_LEFT and KEY_RIGHT to CAPS_LOCK.
170                 if (!isKeyCapsLock(mActivity, mKeyDefaultName)
171                         && isKeyCapsLock(mActivity, selectedItem)) {
172                     inputManager.remapModifierKey(fromKeys[0], toKeys[0]);
173                     inputManager.remapModifierKey(fromKeys[1], toKeys[0]);
174                 }
175                 // Auto handle left and right keys remapping.
176                 if (!isKeyCapsLock(mActivity, mKeyDefaultName)
177                         && !isKeyCapsLock(mActivity, selectedItem)) {
178                     inputManager.remapModifierKey(fromKeys[0], toKeys[0]);
179                     inputManager.remapModifierKey(fromKeys[1], toKeys[1]);
180                 }
181             }
182             dismiss();
183             mActivity.recreate();
184         });
185 
186         Button cancelButton = dialoglayout.findViewById(R.id.modifier_key_cancel_button);
187         cancelButton.setOnClickListener(v -> {
188             dismiss();
189         });
190 
191         final Window window = modifierKeyDialog.getWindow();
192         window.setType(TYPE_SYSTEM_DIALOG);
193 
194         return modifierKeyDialog;
195     }
196 
logMetricsForRemapping(String selectedItem)197     private void logMetricsForRemapping(String selectedItem) {
198         if (mKeyDefaultName.equals("Caps lock")) {
199             mMetricsFeatureProvider.action(
200                     mActivity, SettingsEnums.ACTION_FROM_CAPS_LOCK_TO, selectedItem);
201         }
202 
203         if (mKeyDefaultName.equals("Ctrl")) {
204             mMetricsFeatureProvider.action(
205                     mActivity, SettingsEnums.ACTION_FROM_CTRL_TO, selectedItem);
206         }
207 
208         if (mKeyDefaultName.equals("Action key")) {
209             mMetricsFeatureProvider.action(
210                     mActivity, SettingsEnums.ACTION_FROM_ACTION_KEY_TO, selectedItem);
211         }
212 
213         if (mKeyDefaultName.equals("Alt")) {
214             mMetricsFeatureProvider.action(
215                     mActivity, SettingsEnums.ACTION_FROM_ALT_TO, selectedItem);
216         }
217     }
218 
setInitialFocusItem( List<String> modifierKeys, ModifierKeyAdapter adapter)219     private void setInitialFocusItem(
220             List<String> modifierKeys, ModifierKeyAdapter adapter) {
221         if (modifierKeys.indexOf(mKeyFocus) == -1) {
222             adapter.setCurrentItem(modifierKeys.indexOf(mKeyDefaultName));
223         } else {
224             adapter.setCurrentItem(modifierKeys.indexOf(mKeyFocus));
225         }
226         adapter.notifyDataSetChanged();
227     }
228 
isKeyCapsLock(Context context, String key)229     private static boolean isKeyCapsLock(Context context, String key) {
230         return key.equals(context.getString(R.string.modifier_keys_caps_lock));
231     }
232 
233     class ModifierKeyAdapter extends BaseAdapter {
234         private int mCurrentItem = 0;
235         private List<String> mList;
236 
ModifierKeyAdapter(List<String> list)237         ModifierKeyAdapter(List<String> list) {
238             this.mList = list;
239         }
240 
241         @Override
getCount()242         public int getCount() {
243             return mList.size();
244         }
245 
246         @Override
getItem(int i)247         public Object getItem(int i) {
248             return mList.get(i);
249         }
250 
251         @Override
getItemId(int i)252         public long getItemId(int i) {
253             return i;
254         }
255 
256         @Override
getView(int i, View view, ViewGroup viewGroup)257         public View getView(int i, View view, ViewGroup viewGroup) {
258             if (view == null) {
259                 view = LayoutInflater.from(mActivity).inflate(R.layout.modifier_key_item, null);
260             }
261             TextView textView = view.findViewById(R.id.modifier_key_text);
262             ImageView checkIcon = view.findViewById(R.id.modifier_key_check_icon);
263             textView.setText(mList.get(i));
264             if (mCurrentItem == i) {
265                 mKeyFocus = mList.get(i);
266                 textView.setTextColor(getColorOfMaterialColorPrimary());
267                 checkIcon.setImageAlpha(255);
268                 view.setBackground(
269                         mActivity.getDrawable(R.drawable.modifier_key_lisetview_background));
270                 if (mActionKeyDrawable != null && i == 2) {
271                     setActionKeyIcon(view);
272                     setActionKeyColor(getColorOfMaterialColorPrimary());
273                 }
274             } else {
275                 textView.setTextColor(getColorOfTextColorPrimary());
276                 checkIcon.setImageAlpha(0);
277                 view.setBackground(null);
278                 if (mActionKeyDrawable != null && i == 2) {
279                     setActionKeyIcon(view);
280                     setActionKeyColor(getColorOfTextColorPrimary());
281                 }
282             }
283             return view;
284         }
285 
setCurrentItem(int currentItem)286         public void setCurrentItem(int currentItem) {
287             this.mCurrentItem = currentItem;
288         }
289 
getCurrentItem()290         public int getCurrentItem() {
291             return this.mCurrentItem;
292         }
293     }
294 
setActionKeyIcon(View view)295     private void setActionKeyIcon(View view) {
296         mLeftBracket = view.findViewById(R.id.modifier_key_left_bracket);
297         mRightBracket = view.findViewById(R.id.modifier_key_right_bracket);
298         mActionKeyIcon = view.findViewById(R.id.modifier_key_action_key_icon);
299         mLeftBracket.setText("(");
300         mRightBracket.setText(")");
301         mActionKeyIcon.setImageDrawable(mActionKeyDrawable);
302     }
303 
setActionKeyColor(int color)304     private void setActionKeyColor(int color) {
305         mLeftBracket.setTextColor(color);
306         mRightBracket.setTextColor(color);
307         DrawableCompat.setTint(mActionKeyDrawable, color);
308     }
309 
getColorOfTextColorPrimary()310     private int getColorOfTextColorPrimary() {
311         return Utils.getColorAttrDefaultColor(mActivity, android.R.attr.textColorPrimary);
312     }
313 
getColorOfTextColorSecondary()314     private int getColorOfTextColorSecondary() {
315         return Utils.getColorAttrDefaultColor(mActivity, android.R.attr.textColorSecondary);
316     }
317 
getColorOfMaterialColorPrimary()318     private int getColorOfMaterialColorPrimary() {
319         return Utils.getColorAttrDefaultColor(
320                 mActivity, com.android.internal.R.attr.materialColorPrimary);
321     }
322 }
323