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