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.systemui.car.biometrics;
18 
19 import android.content.Context;
20 import android.content.res.ColorStateList;
21 import android.content.res.TypedArray;
22 import android.util.AttributeSet;
23 import android.view.KeyEvent;
24 import android.view.LayoutInflater;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.widget.GridLayout;
28 import android.widget.ImageButton;
29 import android.widget.TextView;
30 
31 import androidx.annotation.DrawableRes;
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 import androidx.annotation.VisibleForTesting;
35 
36 import com.android.systemui.R;
37 import com.android.systemui.biometrics.ui.IPinPad;
38 import com.android.systemui.biometrics.ui.PinPadClickListener;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 /**
44  * A custom view for the PIN pad.
45  */
46 public class PinPadView extends GridLayout implements IPinPad {
47     // Number of keys in the pin pad, 0-9 plus backspace and enter keys.
48     @VisibleForTesting
49     static final int NUM_KEYS = 12;
50 
51     @VisibleForTesting
52     static final int[] PIN_PAD_DIGIT_KEYS = {R.id.key0, R.id.key1, R.id.key2, R.id.key3,
53             R.id.key4, R.id.key5, R.id.key6, R.id.key7, R.id.key8, R.id.key9};
54 
55     /**
56      * The delay in milliseconds between character deletion when the user continuously holds the
57      * backspace key.
58      */
59     private static final int LONG_CLICK_DELAY_MILLS = 100;
60 
61     private final List<View> mPinKeys = new ArrayList<>(NUM_KEYS);
62     private final Runnable mOnBackspaceLongClick = new Runnable() {
63         public void run() {
64             if (mOnClickListener != null) {
65                 mOnClickListener.onBackspaceClick();
66                 getHandler().postDelayed(this, LONG_CLICK_DELAY_MILLS);
67             }
68         }
69     };
70 
71     private PinPadClickListener mOnClickListener;
72     private ImageButton mEnterKey;
73 
PinPadView(Context context)74     public PinPadView(Context context) {
75         super(context);
76         init(null, 0, 0);
77     }
78 
PinPadView(Context context, AttributeSet attrs)79     public PinPadView(Context context, AttributeSet attrs) {
80         super(context, attrs);
81         init(attrs, 0, 0);
82     }
83 
PinPadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)84     public PinPadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
85         super(context, attrs, defStyleAttr);
86         init(attrs, defStyleAttr, 0);
87     }
88 
PinPadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)89     public PinPadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
90             int defStyleRes) {
91         super(context, attrs, defStyleAttr, defStyleRes);
92         init(attrs, defStyleAttr, defStyleRes);
93     }
94 
95     @Override
setEnabled(boolean enabled)96     public void setEnabled(boolean enabled) {
97         super.setEnabled(enabled);
98         for (View key : mPinKeys) {
99             key.setEnabled(enabled);
100         }
101     }
102 
103     /**
104      * Set the resource Id of the enter key icon.
105      *
106      * @param drawableId The resource Id of the drawable.
107      */
setEnterKeyIcon(@rawableRes int drawableId)108     public void setEnterKeyIcon(@DrawableRes int drawableId) {
109         mEnterKey.setImageResource(drawableId);
110     }
111 
112     /**
113      * Override the default tint of the enter key icon.
114      *
115      * @param tint A ColorStateList.
116      */
setEnterKeyImageTint(ColorStateList tint)117     public void setEnterKeyImageTint(ColorStateList tint) {
118         mEnterKey.setImageTintList(tint);
119     }
120 
121     /**
122      * Sets if the enter key for submitting a PIN is enabled or disabled.
123      */
setEnterKeyEnabled(boolean enabled)124     public void setEnterKeyEnabled(boolean enabled) {
125         mEnterKey.setEnabled(enabled);
126     }
127 
init(AttributeSet attrs, int defStyleAttr, int defStyleRes)128     private void init(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
129         LayoutInflater inflater = LayoutInflater.from(getContext());
130         TypedArray typedArray = getContext().obtainStyledAttributes(
131                 attrs, R.styleable.PinPadView, defStyleAttr, defStyleRes);
132         inflater.inflate(
133                 typedArray.getResourceId(R.styleable.PinPadView_layout, R.layout.pin_pad_view),
134                 this, true);
135         typedArray.recycle();
136 
137         for (int keyId : PIN_PAD_DIGIT_KEYS) {
138             TextView key = findViewById(keyId);
139             String digit = key.getTag().toString();
140             key.setOnClickListener(v -> mOnClickListener.onDigitKeyClick(digit));
141             mPinKeys.add(key);
142         }
143 
144         ImageButton backspace = findViewById(R.id.key_backspace);
145         backspace.setOnTouchListener((v, event) -> {
146             switch (event.getAction()) {
147                 case MotionEvent.ACTION_DOWN:
148                     getHandler().post(mOnBackspaceLongClick);
149                     // Must return false so that ripple can show
150                     return false;
151                 case MotionEvent.ACTION_UP:
152                     getHandler().removeCallbacks(mOnBackspaceLongClick);
153                     // Must return false so that ripple can show
154                     return false;
155                 default:
156                     return false;
157             }
158         });
159         backspace.setOnKeyListener((v, code, event) -> {
160             if (code != KeyEvent.KEYCODE_DPAD_CENTER) {
161                 return false;
162             }
163             switch (event.getAction()) {
164                 case KeyEvent.ACTION_DOWN:
165                     getHandler().post(mOnBackspaceLongClick);
166                     // Must return false so that ripple can show
167                     return false;
168                 case KeyEvent.ACTION_UP:
169                     getHandler().removeCallbacks(mOnBackspaceLongClick);
170                     // Must return false so that ripple can show
171                     return false;
172                 default:
173                     return false;
174             }
175         });
176         mPinKeys.add(backspace);
177 
178         mEnterKey = findViewById(R.id.key_enter);
179         mEnterKey.setOnClickListener(v -> mOnClickListener.onEnterKeyClick());
180 
181         mPinKeys.add(mEnterKey);
182     }
183 
184     @Override
setPinPadClickListener( @onNull PinPadClickListener pinPadClickListener)185     public void setPinPadClickListener(
186             @NonNull PinPadClickListener pinPadClickListener) {
187         mOnClickListener = pinPadClickListener;
188     }
189 }
190