1/*
2 * Copyright (C) 2016 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.systemui.statusbar;
18
19import android.app.AlertDialog;
20import android.app.AppGlobals;
21import android.app.Dialog;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.DialogInterface;
25import android.content.DialogInterface.OnClickListener;
26import android.content.Intent;
27import android.content.pm.IPackageManager;
28import android.content.pm.PackageInfo;
29import android.content.pm.ResolveInfo;
30import android.graphics.drawable.Icon;
31import android.graphics.Bitmap;
32import android.graphics.Canvas;
33import android.graphics.drawable.Drawable;
34import android.hardware.input.InputManager;
35import android.os.Handler;
36import android.os.Looper;
37import android.os.RemoteException;
38import android.util.Log;
39import android.util.SparseArray;
40import android.view.ContextThemeWrapper;
41import android.view.InputDevice;
42import android.view.KeyCharacterMap;
43import android.view.KeyEvent;
44import android.view.KeyboardShortcutGroup;
45import android.view.KeyboardShortcutInfo;
46import android.view.LayoutInflater;
47import android.view.View;
48import android.view.ViewGroup;
49import android.view.Window;
50import android.view.WindowManager.KeyboardShortcutsReceiver;
51import android.widget.ImageView;
52import android.widget.LinearLayout;
53import android.widget.RelativeLayout;
54import android.widget.TextView;
55
56import com.android.internal.app.AssistUtils;
57import com.android.systemui.R;
58import com.android.systemui.recents.Recents;
59
60import java.util.ArrayList;
61import java.util.Collections;
62import java.util.Comparator;
63import java.util.List;
64
65import static android.content.Context.LAYOUT_INFLATER_SERVICE;
66import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
67
68/**
69 * Contains functionality for handling keyboard shortcuts.
70 */
71public final class KeyboardShortcuts {
72    private static final String TAG = KeyboardShortcuts.class.getSimpleName();
73    private static final Object sLock = new Object();
74    private static KeyboardShortcuts sInstance;
75    private static boolean sIsShowing;
76
77    private final SparseArray<String> mSpecialCharacterNames = new SparseArray<>();
78    private final SparseArray<String> mModifierNames = new SparseArray<>();
79    private final SparseArray<Drawable> mSpecialCharacterDrawables = new SparseArray<>();
80    private final SparseArray<Drawable> mModifierDrawables = new SparseArray<>();
81
82    private final Handler mHandler = new Handler(Looper.getMainLooper());
83    private final Context mContext;
84    private final IPackageManager mPackageManager;
85    private final OnClickListener mDialogCloseListener = new DialogInterface.OnClickListener() {
86        public void onClick(DialogInterface dialog, int id) {
87            dismissKeyboardShortcuts();
88        }
89    };
90    private final Comparator<KeyboardShortcutInfo> mApplicationItemsComparator =
91            new Comparator<KeyboardShortcutInfo>() {
92                @Override
93                public int compare(KeyboardShortcutInfo ksh1, KeyboardShortcutInfo ksh2) {
94                    boolean ksh1ShouldBeLast = ksh1.getLabel() == null
95                            || ksh1.getLabel().toString().isEmpty();
96                    boolean ksh2ShouldBeLast = ksh2.getLabel() == null
97                            || ksh2.getLabel().toString().isEmpty();
98                    if (ksh1ShouldBeLast && ksh2ShouldBeLast) {
99                        return 0;
100                    }
101                    if (ksh1ShouldBeLast) {
102                        return 1;
103                    }
104                    if (ksh2ShouldBeLast) {
105                        return -1;
106                    }
107                    return (ksh1.getLabel().toString()).compareToIgnoreCase(
108                            ksh2.getLabel().toString());
109                }
110            };
111
112    private Dialog mKeyboardShortcutsDialog;
113    private KeyCharacterMap mKeyCharacterMap;
114
115    private KeyboardShortcuts(Context context) {
116        this.mContext = new ContextThemeWrapper(context, android.R.style.Theme_Material_Light);
117        this.mPackageManager = AppGlobals.getPackageManager();
118        loadResources(context);
119    }
120
121    private static KeyboardShortcuts getInstance(Context context) {
122        if (sInstance == null) {
123            sInstance = new KeyboardShortcuts(context);
124        }
125        return sInstance;
126    }
127
128    public static void show(Context context, int deviceId) {
129        synchronized (sLock) {
130            if (sInstance != null && !sInstance.mContext.equals(context)) {
131                dismiss();
132            }
133            getInstance(context).showKeyboardShortcuts(deviceId);
134            sIsShowing = true;
135        }
136    }
137
138    public static void toggle(Context context, int deviceId) {
139        synchronized (sLock) {
140            if (sIsShowing) {
141                dismiss();
142            } else {
143                show(context, deviceId);
144            }
145        }
146    }
147
148    public static void dismiss() {
149        synchronized (sLock) {
150            if (sInstance != null) {
151                sInstance.dismissKeyboardShortcuts();
152                sInstance = null;
153            }
154            sIsShowing = false;
155        }
156    }
157
158    private void loadResources(Context context) {
159        mSpecialCharacterNames.put(
160                KeyEvent.KEYCODE_HOME, context.getString(R.string.keyboard_key_home));
161        mSpecialCharacterNames.put(
162                KeyEvent.KEYCODE_BACK, context.getString(R.string.keyboard_key_back));
163        mSpecialCharacterNames.put(
164                KeyEvent.KEYCODE_DPAD_UP, context.getString(R.string.keyboard_key_dpad_up));
165        mSpecialCharacterNames.put(
166                KeyEvent.KEYCODE_DPAD_DOWN, context.getString(R.string.keyboard_key_dpad_down));
167        mSpecialCharacterNames.put(
168                KeyEvent.KEYCODE_DPAD_LEFT, context.getString(R.string.keyboard_key_dpad_left));
169        mSpecialCharacterNames.put(
170                KeyEvent.KEYCODE_DPAD_RIGHT, context.getString(R.string.keyboard_key_dpad_right));
171        mSpecialCharacterNames.put(
172                KeyEvent.KEYCODE_DPAD_CENTER, context.getString(R.string.keyboard_key_dpad_center));
173        mSpecialCharacterNames.put(KeyEvent.KEYCODE_PERIOD, ".");
174        mSpecialCharacterNames.put(
175                KeyEvent.KEYCODE_TAB, context.getString(R.string.keyboard_key_tab));
176        mSpecialCharacterNames.put(
177                KeyEvent.KEYCODE_SPACE, context.getString(R.string.keyboard_key_space));
178        mSpecialCharacterNames.put(
179                KeyEvent.KEYCODE_ENTER, context.getString(R.string.keyboard_key_enter));
180        mSpecialCharacterNames.put(
181                KeyEvent.KEYCODE_DEL, context.getString(R.string.keyboard_key_backspace));
182        mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
183                context.getString(R.string.keyboard_key_media_play_pause));
184        mSpecialCharacterNames.put(
185                KeyEvent.KEYCODE_MEDIA_STOP, context.getString(R.string.keyboard_key_media_stop));
186        mSpecialCharacterNames.put(
187                KeyEvent.KEYCODE_MEDIA_NEXT, context.getString(R.string.keyboard_key_media_next));
188        mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_PREVIOUS,
189                context.getString(R.string.keyboard_key_media_previous));
190        mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_REWIND,
191                context.getString(R.string.keyboard_key_media_rewind));
192        mSpecialCharacterNames.put(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD,
193                context.getString(R.string.keyboard_key_media_fast_forward));
194        mSpecialCharacterNames.put(
195                KeyEvent.KEYCODE_PAGE_UP, context.getString(R.string.keyboard_key_page_up));
196        mSpecialCharacterNames.put(
197                KeyEvent.KEYCODE_PAGE_DOWN, context.getString(R.string.keyboard_key_page_down));
198        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_A,
199                context.getString(R.string.keyboard_key_button_template, "A"));
200        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_B,
201                context.getString(R.string.keyboard_key_button_template, "B"));
202        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_C,
203                context.getString(R.string.keyboard_key_button_template, "C"));
204        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_X,
205                context.getString(R.string.keyboard_key_button_template, "X"));
206        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_Y,
207                context.getString(R.string.keyboard_key_button_template, "Y"));
208        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_Z,
209                context.getString(R.string.keyboard_key_button_template, "Z"));
210        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_L1,
211                context.getString(R.string.keyboard_key_button_template, "L1"));
212        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_R1,
213                context.getString(R.string.keyboard_key_button_template, "R1"));
214        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_L2,
215                context.getString(R.string.keyboard_key_button_template, "L2"));
216        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_R2,
217                context.getString(R.string.keyboard_key_button_template, "R2"));
218        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_START,
219                context.getString(R.string.keyboard_key_button_template, "Start"));
220        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_SELECT,
221                context.getString(R.string.keyboard_key_button_template, "Select"));
222        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BUTTON_MODE,
223                context.getString(R.string.keyboard_key_button_template, "Mode"));
224        mSpecialCharacterNames.put(
225                KeyEvent.KEYCODE_FORWARD_DEL, context.getString(R.string.keyboard_key_forward_del));
226        mSpecialCharacterNames.put(KeyEvent.KEYCODE_ESCAPE, "Esc");
227        mSpecialCharacterNames.put(KeyEvent.KEYCODE_SYSRQ, "SysRq");
228        mSpecialCharacterNames.put(KeyEvent.KEYCODE_BREAK, "Break");
229        mSpecialCharacterNames.put(KeyEvent.KEYCODE_SCROLL_LOCK, "Scroll Lock");
230        mSpecialCharacterNames.put(
231                KeyEvent.KEYCODE_MOVE_HOME, context.getString(R.string.keyboard_key_move_home));
232        mSpecialCharacterNames.put(
233                KeyEvent.KEYCODE_MOVE_END, context.getString(R.string.keyboard_key_move_end));
234        mSpecialCharacterNames.put(
235                KeyEvent.KEYCODE_INSERT, context.getString(R.string.keyboard_key_insert));
236        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F1, "F1");
237        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F2, "F2");
238        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F3, "F3");
239        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F4, "F4");
240        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F5, "F5");
241        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F6, "F6");
242        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F7, "F7");
243        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F8, "F8");
244        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F9, "F9");
245        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F10, "F10");
246        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F11, "F11");
247        mSpecialCharacterNames.put(KeyEvent.KEYCODE_F12, "F12");
248        mSpecialCharacterNames.put(
249                KeyEvent.KEYCODE_NUM_LOCK, context.getString(R.string.keyboard_key_num_lock));
250        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_0,
251                context.getString(R.string.keyboard_key_numpad_template, "0"));
252        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_1,
253                context.getString(R.string.keyboard_key_numpad_template, "1"));
254        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_2,
255                context.getString(R.string.keyboard_key_numpad_template, "2"));
256        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_3,
257                context.getString(R.string.keyboard_key_numpad_template, "3"));
258        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_4,
259                context.getString(R.string.keyboard_key_numpad_template, "4"));
260        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_5,
261                context.getString(R.string.keyboard_key_numpad_template, "5"));
262        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_6,
263                context.getString(R.string.keyboard_key_numpad_template, "6"));
264        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_7,
265                context.getString(R.string.keyboard_key_numpad_template, "7"));
266        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_8,
267                context.getString(R.string.keyboard_key_numpad_template, "8"));
268        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_9,
269                context.getString(R.string.keyboard_key_numpad_template, "9"));
270        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_DIVIDE,
271                context.getString(R.string.keyboard_key_numpad_template, "/"));
272        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
273                context.getString(R.string.keyboard_key_numpad_template, "*"));
274        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
275                context.getString(R.string.keyboard_key_numpad_template, "-"));
276        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_ADD,
277                context.getString(R.string.keyboard_key_numpad_template, "+"));
278        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_DOT,
279                context.getString(R.string.keyboard_key_numpad_template, "."));
280        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_COMMA,
281                context.getString(R.string.keyboard_key_numpad_template, ","));
282        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_ENTER,
283                context.getString(R.string.keyboard_key_numpad_template,
284                        context.getString(R.string.keyboard_key_enter)));
285        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_EQUALS,
286                context.getString(R.string.keyboard_key_numpad_template, "="));
287        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_LEFT_PAREN,
288                context.getString(R.string.keyboard_key_numpad_template, "("));
289        mSpecialCharacterNames.put(KeyEvent.KEYCODE_NUMPAD_RIGHT_PAREN,
290                context.getString(R.string.keyboard_key_numpad_template, ")"));
291        mSpecialCharacterNames.put(KeyEvent.KEYCODE_ZENKAKU_HANKAKU, "半角/全角");
292        mSpecialCharacterNames.put(KeyEvent.KEYCODE_EISU, "英数");
293        mSpecialCharacterNames.put(KeyEvent.KEYCODE_MUHENKAN, "無変換");
294        mSpecialCharacterNames.put(KeyEvent.KEYCODE_HENKAN, "変換");
295        mSpecialCharacterNames.put(KeyEvent.KEYCODE_KATAKANA_HIRAGANA, "かな");
296
297        mModifierNames.put(KeyEvent.META_META_ON, "Meta");
298        mModifierNames.put(KeyEvent.META_CTRL_ON, "Ctrl");
299        mModifierNames.put(KeyEvent.META_ALT_ON, "Alt");
300        mModifierNames.put(KeyEvent.META_SHIFT_ON, "Shift");
301        mModifierNames.put(KeyEvent.META_SYM_ON, "Sym");
302        mModifierNames.put(KeyEvent.META_FUNCTION_ON, "Fn");
303
304        mSpecialCharacterDrawables.put(
305                KeyEvent.KEYCODE_DEL, context.getDrawable(R.drawable.ic_ksh_key_backspace));
306        mSpecialCharacterDrawables.put(
307                KeyEvent.KEYCODE_ENTER, context.getDrawable(R.drawable.ic_ksh_key_enter));
308        mSpecialCharacterDrawables.put(
309                KeyEvent.KEYCODE_DPAD_UP, context.getDrawable(R.drawable.ic_ksh_key_up));
310        mSpecialCharacterDrawables.put(
311                KeyEvent.KEYCODE_DPAD_RIGHT, context.getDrawable(R.drawable.ic_ksh_key_right));
312        mSpecialCharacterDrawables.put(
313                KeyEvent.KEYCODE_DPAD_DOWN, context.getDrawable(R.drawable.ic_ksh_key_down));
314        mSpecialCharacterDrawables.put(
315                KeyEvent.KEYCODE_DPAD_LEFT, context.getDrawable(R.drawable.ic_ksh_key_left));
316
317        mModifierDrawables.put(
318                KeyEvent.META_META_ON, context.getDrawable(R.drawable.ic_ksh_key_meta));
319    }
320
321    /**
322     * Retrieves a {@link KeyCharacterMap} and assigns it to mKeyCharacterMap. If the given id is an
323     * existing device, that device's map is used. Otherwise, it checks first all available devices
324     * and if there is a full keyboard it uses that map, otherwise falls back to the Virtual
325     * Keyboard with its default map.
326     */
327    private void retrieveKeyCharacterMap(int deviceId) {
328        final InputManager inputManager = InputManager.getInstance();
329        if (deviceId != -1) {
330            final InputDevice inputDevice = inputManager.getInputDevice(deviceId);
331            if (inputDevice != null) {
332                mKeyCharacterMap = inputDevice.getKeyCharacterMap();
333                return;
334            }
335        }
336        final int[] deviceIds = inputManager.getInputDeviceIds();
337        for (int i = 0; i < deviceIds.length; ++i) {
338            final InputDevice inputDevice = inputManager.getInputDevice(deviceIds[i]);
339            // -1 is the Virtual Keyboard, with the default key map. Use that one only as last
340            // resort.
341            if (inputDevice.getId() != -1 && inputDevice.isFullKeyboard()) {
342                mKeyCharacterMap = inputDevice.getKeyCharacterMap();
343                return;
344            }
345        }
346        final InputDevice inputDevice = inputManager.getInputDevice(-1);
347        mKeyCharacterMap = inputDevice.getKeyCharacterMap();
348    }
349
350    private void showKeyboardShortcuts(int deviceId) {
351        retrieveKeyCharacterMap(deviceId);
352        Recents.getSystemServices().requestKeyboardShortcuts(mContext,
353                new KeyboardShortcutsReceiver() {
354                    @Override
355                    public void onKeyboardShortcutsReceived(
356                            final List<KeyboardShortcutGroup> result) {
357                        result.add(getSystemShortcuts());
358                        final KeyboardShortcutGroup appShortcuts = getDefaultApplicationShortcuts();
359                        if (appShortcuts != null) {
360                            result.add(appShortcuts);
361                        }
362                        showKeyboardShortcutsDialog(result);
363                    }
364                }, deviceId);
365    }
366
367    private void dismissKeyboardShortcuts() {
368        if (mKeyboardShortcutsDialog != null) {
369            mKeyboardShortcutsDialog.dismiss();
370            mKeyboardShortcutsDialog = null;
371        }
372    }
373
374    private KeyboardShortcutGroup getSystemShortcuts() {
375        final KeyboardShortcutGroup systemGroup = new KeyboardShortcutGroup(
376                mContext.getString(R.string.keyboard_shortcut_group_system), true);
377        systemGroup.addItem(new KeyboardShortcutInfo(
378                mContext.getString(R.string.keyboard_shortcut_group_system_home),
379                KeyEvent.KEYCODE_ENTER,
380                KeyEvent.META_META_ON));
381        systemGroup.addItem(new KeyboardShortcutInfo(
382                mContext.getString(R.string.keyboard_shortcut_group_system_back),
383                KeyEvent.KEYCODE_DEL,
384                KeyEvent.META_META_ON));
385        systemGroup.addItem(new KeyboardShortcutInfo(
386                mContext.getString(R.string.keyboard_shortcut_group_system_recents),
387                KeyEvent.KEYCODE_TAB,
388                KeyEvent.META_ALT_ON));
389        systemGroup.addItem(new KeyboardShortcutInfo(
390                mContext.getString(
391                        R.string.keyboard_shortcut_group_system_notifications),
392                KeyEvent.KEYCODE_N,
393                KeyEvent.META_META_ON));
394        systemGroup.addItem(new KeyboardShortcutInfo(
395                mContext.getString(
396                        R.string.keyboard_shortcut_group_system_shortcuts_helper),
397                KeyEvent.KEYCODE_SLASH,
398                KeyEvent.META_META_ON));
399        systemGroup.addItem(new KeyboardShortcutInfo(
400                mContext.getString(
401                        R.string.keyboard_shortcut_group_system_switch_input),
402                KeyEvent.KEYCODE_SPACE,
403                KeyEvent.META_META_ON));
404        return systemGroup;
405    }
406
407    private KeyboardShortcutGroup getDefaultApplicationShortcuts() {
408        final int userId = mContext.getUserId();
409        List<KeyboardShortcutInfo> keyboardShortcutInfoAppItems = new ArrayList<>();
410
411        // Assist.
412        final AssistUtils assistUtils = new AssistUtils(mContext);
413        final ComponentName assistComponent = assistUtils.getAssistComponentForUser(userId);
414        PackageInfo assistPackageInfo = null;
415        try {
416            assistPackageInfo = mPackageManager.getPackageInfo(
417                    assistComponent.getPackageName(), 0, userId);
418        } catch (RemoteException e) {
419            Log.e(TAG, "PackageManagerService is dead");
420        }
421
422        if (assistPackageInfo != null) {
423            final Icon assistIcon = Icon.createWithResource(
424                    assistPackageInfo.applicationInfo.packageName,
425                    assistPackageInfo.applicationInfo.icon);
426
427            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
428                    mContext.getString(R.string.keyboard_shortcut_group_applications_assist),
429                    assistIcon,
430                    KeyEvent.KEYCODE_UNKNOWN,
431                    KeyEvent.META_META_ON));
432        }
433
434        // Browser.
435        final Icon browserIcon = getIconForIntentCategory(Intent.CATEGORY_APP_BROWSER, userId);
436        if (browserIcon != null) {
437            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
438                    mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
439                    browserIcon,
440                    KeyEvent.KEYCODE_B,
441                    KeyEvent.META_META_ON));
442        }
443
444
445        // Contacts.
446        final Icon contactsIcon = getIconForIntentCategory(Intent.CATEGORY_APP_CONTACTS, userId);
447        if (contactsIcon != null) {
448            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
449                    mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
450                    contactsIcon,
451                    KeyEvent.KEYCODE_C,
452                    KeyEvent.META_META_ON));
453        }
454
455        // Email.
456        final Icon emailIcon = getIconForIntentCategory(Intent.CATEGORY_APP_EMAIL, userId);
457        if (emailIcon != null) {
458            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
459                    mContext.getString(R.string.keyboard_shortcut_group_applications_email),
460                    emailIcon,
461                    KeyEvent.KEYCODE_E,
462                    KeyEvent.META_META_ON));
463        }
464
465        // Messaging.
466        final Icon messagingIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MESSAGING, userId);
467        if (messagingIcon != null) {
468            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
469                    mContext.getString(R.string.keyboard_shortcut_group_applications_im),
470                    messagingIcon,
471                    KeyEvent.KEYCODE_T,
472                    KeyEvent.META_META_ON));
473        }
474
475        // Music.
476        final Icon musicIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MUSIC, userId);
477        if (musicIcon != null) {
478            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
479                    mContext.getString(R.string.keyboard_shortcut_group_applications_music),
480                    musicIcon,
481                    KeyEvent.KEYCODE_P,
482                    KeyEvent.META_META_ON));
483        }
484
485        // Calendar.
486        final Icon calendarIcon = getIconForIntentCategory(Intent.CATEGORY_APP_CALENDAR, userId);
487        if (calendarIcon != null) {
488            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
489                    mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
490                    calendarIcon,
491                    KeyEvent.KEYCODE_L,
492                    KeyEvent.META_META_ON));
493        }
494
495        final int itemsSize = keyboardShortcutInfoAppItems.size();
496        if (itemsSize == 0) {
497            return null;
498        }
499
500        // Sorts by label, case insensitive with nulls and/or empty labels last.
501        Collections.sort(keyboardShortcutInfoAppItems, mApplicationItemsComparator);
502        return new KeyboardShortcutGroup(
503                mContext.getString(R.string.keyboard_shortcut_group_applications),
504                keyboardShortcutInfoAppItems,
505                true);
506    }
507
508    private Icon getIconForIntentCategory(String intentCategory, int userId) {
509        final Intent intent = new Intent(Intent.ACTION_MAIN);
510        intent.addCategory(intentCategory);
511
512        final PackageInfo packageInfo = getPackageInfoForIntent(intent, userId);
513        if (packageInfo != null && packageInfo.applicationInfo.icon != 0) {
514            return Icon.createWithResource(
515                    packageInfo.applicationInfo.packageName,
516                    packageInfo.applicationInfo.icon);
517        }
518        return null;
519    }
520
521    private PackageInfo getPackageInfoForIntent(Intent intent, int userId) {
522        try {
523            ResolveInfo handler;
524            handler = mPackageManager.resolveIntent(
525                    intent, intent.resolveTypeIfNeeded(mContext.getContentResolver()), 0, userId);
526            if (handler == null || handler.activityInfo == null) {
527                return null;
528            }
529            return mPackageManager.getPackageInfo(handler.activityInfo.packageName, 0, userId);
530        } catch (RemoteException e) {
531            Log.e(TAG, "PackageManagerService is dead", e);
532            return null;
533        }
534    }
535
536    private void showKeyboardShortcutsDialog(
537            final List<KeyboardShortcutGroup> keyboardShortcutGroups) {
538        // Need to post on the main thread.
539        mHandler.post(new Runnable() {
540            @Override
541            public void run() {
542                handleShowKeyboardShortcuts(keyboardShortcutGroups);
543            }
544        });
545    }
546
547    private void handleShowKeyboardShortcuts(List<KeyboardShortcutGroup> keyboardShortcutGroups) {
548        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext);
549        LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
550                LAYOUT_INFLATER_SERVICE);
551        final View keyboardShortcutsView = inflater.inflate(
552                R.layout.keyboard_shortcuts_view, null);
553        populateKeyboardShortcuts((LinearLayout) keyboardShortcutsView.findViewById(
554                R.id.keyboard_shortcuts_container), keyboardShortcutGroups);
555        dialogBuilder.setView(keyboardShortcutsView);
556        dialogBuilder.setPositiveButton(R.string.quick_settings_done, mDialogCloseListener);
557        mKeyboardShortcutsDialog = dialogBuilder.create();
558        mKeyboardShortcutsDialog.setCanceledOnTouchOutside(true);
559        Window keyboardShortcutsWindow = mKeyboardShortcutsDialog.getWindow();
560        keyboardShortcutsWindow.setType(TYPE_SYSTEM_DIALOG);
561        mKeyboardShortcutsDialog.show();
562    }
563
564    private void populateKeyboardShortcuts(LinearLayout keyboardShortcutsLayout,
565            List<KeyboardShortcutGroup> keyboardShortcutGroups) {
566        LayoutInflater inflater = LayoutInflater.from(mContext);
567        final int keyboardShortcutGroupsSize = keyboardShortcutGroups.size();
568        TextView shortcutsKeyView = (TextView) inflater.inflate(
569                R.layout.keyboard_shortcuts_key_view, null, false);
570        shortcutsKeyView.measure(
571                View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
572        final int shortcutKeyTextItemMinWidth = shortcutsKeyView.getMeasuredHeight();
573        // Needed to be able to scale the image items to the same height as the text items.
574        final int shortcutKeyIconItemHeightWidth = shortcutsKeyView.getMeasuredHeight()
575                - shortcutsKeyView.getPaddingTop()
576                - shortcutsKeyView.getPaddingBottom();
577        for (int i = 0; i < keyboardShortcutGroupsSize; i++) {
578            KeyboardShortcutGroup group = keyboardShortcutGroups.get(i);
579            TextView categoryTitle = (TextView) inflater.inflate(
580                    R.layout.keyboard_shortcuts_category_title, keyboardShortcutsLayout, false);
581            categoryTitle.setText(group.getLabel());
582            categoryTitle.setTextColor(group.isSystemGroup()
583                    ? mContext.getColor(R.color.ksh_system_group_color)
584                    : mContext.getColor(R.color.ksh_application_group_color));
585            keyboardShortcutsLayout.addView(categoryTitle);
586
587            LinearLayout shortcutContainer = (LinearLayout) inflater.inflate(
588                    R.layout.keyboard_shortcuts_container, keyboardShortcutsLayout, false);
589            final int itemsSize = group.getItems().size();
590            for (int j = 0; j < itemsSize; j++) {
591                KeyboardShortcutInfo info = group.getItems().get(j);
592                List<StringOrDrawable> shortcutKeys = getHumanReadableShortcutKeys(info);
593                if (shortcutKeys == null) {
594                    // Ignore shortcuts we can't display keys for.
595                    Log.w(TAG, "Keyboard Shortcut contains unsupported keys, skipping.");
596                    continue;
597                }
598                View shortcutView = inflater.inflate(R.layout.keyboard_shortcut_app_item,
599                        shortcutContainer, false);
600
601                if (info.getIcon() != null) {
602                    ImageView shortcutIcon = (ImageView) shortcutView
603                            .findViewById(R.id.keyboard_shortcuts_icon);
604                    shortcutIcon.setImageIcon(info.getIcon());
605                    shortcutIcon.setVisibility(View.VISIBLE);
606                }
607
608                TextView shortcutKeyword = (TextView) shortcutView
609                        .findViewById(R.id.keyboard_shortcuts_keyword);
610                shortcutKeyword.setText(info.getLabel());
611                if (info.getIcon() != null) {
612                    RelativeLayout.LayoutParams lp =
613                            (RelativeLayout.LayoutParams) shortcutKeyword.getLayoutParams();
614                    lp.removeRule(RelativeLayout.ALIGN_PARENT_START);
615                    shortcutKeyword.setLayoutParams(lp);
616                }
617
618                ViewGroup shortcutItemsContainer = (ViewGroup) shortcutView
619                        .findViewById(R.id.keyboard_shortcuts_item_container);
620                final int shortcutKeysSize = shortcutKeys.size();
621                for (int k = 0; k < shortcutKeysSize; k++) {
622                    StringOrDrawable shortcutRepresentation = shortcutKeys.get(k);
623                    if (shortcutRepresentation.drawable != null) {
624                        ImageView shortcutKeyIconView = (ImageView) inflater.inflate(
625                                R.layout.keyboard_shortcuts_key_icon_view, shortcutItemsContainer,
626                                false);
627                        Bitmap bitmap = Bitmap.createBitmap(shortcutKeyIconItemHeightWidth,
628                                shortcutKeyIconItemHeightWidth, Bitmap.Config.ARGB_8888);
629                        Canvas canvas = new Canvas(bitmap);
630                        shortcutRepresentation.drawable.setBounds(0, 0, canvas.getWidth(),
631                                canvas.getHeight());
632                        shortcutRepresentation.drawable.draw(canvas);
633                        shortcutKeyIconView.setImageBitmap(bitmap);
634                        shortcutItemsContainer.addView(shortcutKeyIconView);
635                    } else if (shortcutRepresentation.string != null) {
636                        TextView shortcutKeyTextView = (TextView) inflater.inflate(
637                                R.layout.keyboard_shortcuts_key_view, shortcutItemsContainer,
638                                false);
639                        shortcutKeyTextView.setMinimumWidth(shortcutKeyTextItemMinWidth);
640                        shortcutKeyTextView.setText(shortcutRepresentation.string);
641                        shortcutItemsContainer.addView(shortcutKeyTextView);
642                    }
643                }
644                shortcutContainer.addView(shortcutView);
645            }
646            keyboardShortcutsLayout.addView(shortcutContainer);
647            if (i < keyboardShortcutGroupsSize - 1) {
648                View separator = inflater.inflate(
649                        R.layout.keyboard_shortcuts_category_separator, keyboardShortcutsLayout,
650                        false);
651                keyboardShortcutsLayout.addView(separator);
652            }
653        }
654    }
655
656    private List<StringOrDrawable> getHumanReadableShortcutKeys(KeyboardShortcutInfo info) {
657        List<StringOrDrawable> shortcutKeys = getHumanReadableModifiers(info);
658        if (shortcutKeys == null) {
659            return null;
660        }
661        String displayLabelString = null;
662        Drawable displayLabelDrawable = null;
663        if (info.getBaseCharacter() > Character.MIN_VALUE) {
664            displayLabelString = String.valueOf(info.getBaseCharacter());
665        } else if (mSpecialCharacterDrawables.get(info.getKeycode()) != null) {
666            displayLabelDrawable = mSpecialCharacterDrawables.get(info.getKeycode());
667        } else if (mSpecialCharacterNames.get(info.getKeycode()) != null) {
668            displayLabelString = mSpecialCharacterNames.get(info.getKeycode());
669        } else {
670            // Special case for shortcuts with no base key or keycode.
671            if (info.getKeycode() == KeyEvent.KEYCODE_UNKNOWN) {
672                return shortcutKeys;
673            }
674            char displayLabel = mKeyCharacterMap.getDisplayLabel(info.getKeycode());
675            if (displayLabel != 0) {
676                displayLabelString = String.valueOf(displayLabel);
677            } else {
678                return null;
679            }
680        }
681
682        if (displayLabelDrawable != null) {
683            shortcutKeys.add(new StringOrDrawable(displayLabelDrawable));
684        } else if (displayLabelString != null) {
685            shortcutKeys.add(new StringOrDrawable(displayLabelString.toUpperCase()));
686        }
687        return shortcutKeys;
688    }
689
690    private List<StringOrDrawable> getHumanReadableModifiers(KeyboardShortcutInfo info) {
691        final List<StringOrDrawable> shortcutKeys = new ArrayList<>();
692        int modifiers = info.getModifiers();
693        if (modifiers == 0) {
694            return shortcutKeys;
695        }
696        for(int i = 0; i < mModifierNames.size(); ++i) {
697            final int supportedModifier = mModifierNames.keyAt(i);
698            if ((modifiers & supportedModifier) != 0) {
699                if (mModifierDrawables.get(supportedModifier) != null) {
700                    shortcutKeys.add(new StringOrDrawable(
701                            mModifierDrawables.get(supportedModifier)));
702                } else {
703                    shortcutKeys.add(new StringOrDrawable(
704                            mModifierNames.get(supportedModifier).toUpperCase()));
705                }
706                modifiers &= ~supportedModifier;
707            }
708        }
709        if (modifiers != 0) {
710            // Remaining unsupported modifiers, don't show anything.
711            return null;
712        }
713        return shortcutKeys;
714    }
715
716    private static final class StringOrDrawable {
717        public String string;
718        public Drawable drawable;
719
720        public StringOrDrawable(String string) {
721            this.string = string;
722        }
723
724        public StringOrDrawable(Drawable drawable) {
725            this.drawable = drawable;
726        }
727    }
728}
729