1 /*
2  * Copyright (C) 2018 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.car.dialer.ui.dialpad;
18 
19 import static com.android.car.dialer.ui.dialpad.DialpadRestrictionViewModel.shouldEnforceNoDialpadRestriction;
20 
21 import android.os.Bundle;
22 import android.util.SparseArray;
23 import android.view.KeyEvent;
24 import android.view.LayoutInflater;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.view.ViewGroup;
28 
29 import androidx.annotation.IntDef;
30 import androidx.annotation.NonNull;
31 import androidx.annotation.Nullable;
32 import androidx.fragment.app.Fragment;
33 import androidx.lifecycle.ViewModelProvider;
34 
35 import com.android.car.dialer.R;
36 import com.android.car.dialer.ui.dialpad.DialpadRestrictionViewModel.DialpadUxrMode;
37 
38 import java.lang.annotation.Retention;
39 import java.lang.annotation.RetentionPolicy;
40 
41 /**
42  * Fragment which displays a pad of keys.
43  */
44 public class KeypadFragment extends Fragment {
45     private static final SparseArray<Integer> sRIdMap = new SparseArray<>();
46 
47     static {
sRIdMap.put(KeyEvent.KEYCODE_1, R.id.one)48         sRIdMap.put(KeyEvent.KEYCODE_1, R.id.one);
sRIdMap.put(KeyEvent.KEYCODE_2, R.id.two)49         sRIdMap.put(KeyEvent.KEYCODE_2, R.id.two);
sRIdMap.put(KeyEvent.KEYCODE_3, R.id.three)50         sRIdMap.put(KeyEvent.KEYCODE_3, R.id.three);
sRIdMap.put(KeyEvent.KEYCODE_4, R.id.four)51         sRIdMap.put(KeyEvent.KEYCODE_4, R.id.four);
sRIdMap.put(KeyEvent.KEYCODE_5, R.id.five)52         sRIdMap.put(KeyEvent.KEYCODE_5, R.id.five);
sRIdMap.put(KeyEvent.KEYCODE_6, R.id.six)53         sRIdMap.put(KeyEvent.KEYCODE_6, R.id.six);
sRIdMap.put(KeyEvent.KEYCODE_7, R.id.seven)54         sRIdMap.put(KeyEvent.KEYCODE_7, R.id.seven);
sRIdMap.put(KeyEvent.KEYCODE_8, R.id.eight)55         sRIdMap.put(KeyEvent.KEYCODE_8, R.id.eight);
sRIdMap.put(KeyEvent.KEYCODE_9, R.id.nine)56         sRIdMap.put(KeyEvent.KEYCODE_9, R.id.nine);
sRIdMap.put(KeyEvent.KEYCODE_0, R.id.zero)57         sRIdMap.put(KeyEvent.KEYCODE_0, R.id.zero);
sRIdMap.put(KeyEvent.KEYCODE_STAR, R.id.star)58         sRIdMap.put(KeyEvent.KEYCODE_STAR, R.id.star);
sRIdMap.put(KeyEvent.KEYCODE_POUND, R.id.pound)59         sRIdMap.put(KeyEvent.KEYCODE_POUND, R.id.pound);
60     }
61 
62     /** Valid keycodes that can be sent to the callback. **/
63     @Retention(RetentionPolicy.SOURCE)
64     @IntDef({KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_3,
65             KeyEvent.KEYCODE_4, KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_6, KeyEvent.KEYCODE_7,
66             KeyEvent.KEYCODE_8, KeyEvent.KEYCODE_9, KeyEvent.KEYCODE_STAR, KeyEvent.KEYCODE_POUND})
67     @interface DialKeyCode {
68     }
69 
70     /** Callback for keypad to interact with its host. */
71     public interface KeypadCallback {
72 
73         /** Called when a key is long pressed. */
onKeypadKeyLongPressed(@ialKeyCode int keycode)74         void onKeypadKeyLongPressed(@DialKeyCode int keycode);
75 
76         /** Called when a key is pressed down. */
onKeypadKeyDown(@ialKeyCode int keycode)77         void onKeypadKeyDown(@DialKeyCode int keycode);
78 
79         /** Called when a key is released. */
onKeypadKeyUp(@ialKeyCode int keycode)80         void onKeypadKeyUp(@DialKeyCode int keycode);
81     }
82 
83     private KeypadCallback mKeypadCallback;
84 
85     @Override
onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)86     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
87             @Nullable Bundle savedInstanceState) {
88         if (getParentFragment() instanceof KeypadCallback) {
89             mKeypadCallback = (KeypadCallback) getParentFragment();
90         } else if (getHost() instanceof KeypadCallback) {
91             mKeypadCallback = (KeypadCallback) getHost();
92         }
93 
94         View keypadView = inflater.inflate(R.layout.keypad, container, false);
95         setupKeypadClickListeners(keypadView);
96 
97         if (shouldEnforceNoDialpadRestriction(getContext())) {
98             DialpadRestrictionViewModel restrictionViewModel =
99                     new ViewModelProvider(getActivity()).get(DialpadRestrictionViewModel.class);
100 
101             boolean isInCallKeypad = getParentFragment() instanceof InCallDialpadFragment;
102             restrictionViewModel.getDialpadMode().observe(
103                     this,
104                     dialpadUxrMode -> {
105                         if (isInCallKeypad) {
106                             // Disable the keypad in-call, if any restriction level is active.
107                             setKeypadEnabled(dialpadUxrMode == DialpadUxrMode.UNRESTRICTED);
108                         } else {
109                             setKeypadEnabled(isDialingAllowed(dialpadUxrMode));
110                         }
111                     });
112         }
113 
114         return keypadView;
115     }
116 
isDialingAllowed(DialpadUxrMode dialpadUxrMode)117     private boolean isDialingAllowed(DialpadUxrMode dialpadUxrMode) {
118         return dialpadUxrMode != DialpadUxrMode.DISABLED
119                 && dialpadUxrMode != DialpadUxrMode.RESTRICTED_LIMIT_REACHED;
120     }
121 
122     /**
123      * The click listener for all keypad buttons.  Reacts to touch-down and touch-up events, as
124      * well as long-press for certain keys.  Mimics the behavior of the phone dialer app.
125      */
126     private class KeypadClickListener implements View.OnTouchListener,
127             View.OnLongClickListener, View.OnKeyListener, View.OnFocusChangeListener {
128         private final int mKeycode;
129         private boolean mIsKeyDown = false;
130 
KeypadClickListener(@ialKeyCode int keyCode)131         KeypadClickListener(@DialKeyCode int keyCode) {
132             mKeycode = keyCode;
133         }
134 
135         @Override
onLongClick(View v)136         public boolean onLongClick(View v) {
137             mKeypadCallback.onKeypadKeyLongPressed(mKeycode);
138             return true;
139         }
140 
141         @Override
onTouch(View v, MotionEvent event)142         public boolean onTouch(View v, MotionEvent event) {
143             if (mKeypadCallback != null) {
144                 if (event.getAction() == MotionEvent.ACTION_DOWN) {
145                     mKeypadCallback.onKeypadKeyDown(mKeycode);
146                 } else if (event.getAction() == MotionEvent.ACTION_UP) {
147                     mKeypadCallback.onKeypadKeyUp(mKeycode);
148                 }
149             }
150 
151             // Continue propagating the event
152             return false;
153         }
154 
155         @Override
onKey(View v, int keyCode, KeyEvent event)156         public boolean onKey(View v, int keyCode, KeyEvent event) {
157             if (mKeypadCallback != null && isConfirmKey(keyCode)) {
158                 if (event.getAction() == KeyEvent.ACTION_DOWN && !mIsKeyDown) {
159                     mIsKeyDown = true;
160                     mKeypadCallback.onKeypadKeyDown(mKeycode);
161                 } else if (event.getAction() == KeyEvent.ACTION_UP && mIsKeyDown) {
162                     mIsKeyDown = false;
163                     mKeypadCallback.onKeypadKeyUp(mKeycode);
164                 }
165             }
166 
167             // Continue propagating the event
168             return false;
169         }
170 
171         @Override
onFocusChange(View v, boolean hasFocus)172         public void onFocusChange(View v, boolean hasFocus) {
173             if (!hasFocus && mIsKeyDown) {
174                 mIsKeyDown = false;
175                 mKeypadCallback.onKeypadKeyUp(mKeycode);
176             }
177         }
178     }
179 
setupKeypadClickListeners(View parent)180     private void setupKeypadClickListeners(View parent) {
181         for (int i = 0; i < sRIdMap.size(); i++) {
182             int key = sRIdMap.keyAt(i);
183             KeypadClickListener clickListener = new KeypadClickListener(key);
184             View v = parent.findViewById(sRIdMap.get(key));
185             v.setOnTouchListener(clickListener);
186             v.setOnLongClickListener(clickListener);
187             v.setOnKeyListener(clickListener);
188             v.setOnFocusChangeListener(clickListener);
189         }
190     }
191 
setKeypadEnabled(boolean enabled)192     private void setKeypadEnabled(boolean enabled) {
193         View rootView = getView();
194         for (int i = 0; i < sRIdMap.size(); i++) {
195             rootView.findViewById(sRIdMap.valueAt(i)).setEnabled(enabled);
196         }
197     }
198 
isConfirmKey(int keyCode)199     private boolean isConfirmKey(int keyCode) {
200         switch (keyCode) {
201             case KeyEvent.KEYCODE_DPAD_CENTER:
202             case KeyEvent.KEYCODE_ENTER:
203             case KeyEvent.KEYCODE_SPACE:
204             case KeyEvent.KEYCODE_NUMPAD_ENTER:
205                 return true;
206             default:
207                 return false;
208         }
209     }
210 }
211