Reduced duplicated code with a common TextWatcher implementation for all the input...
[pub/Android/ownCloud.git] / src / com / owncloud / android / ui / activity / PassCodeActivity.java
1 /**
2 * ownCloud Android client application
3 *
4 * @author Bartek Przybylski
5 * @author masensio
6 * @author David A. Velasco
7 * Copyright (C) 2011 Bartek Przybylski
8 * Copyright (C) 2015 ownCloud Inc.
9 *
10 * This program is free software: you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 *
22 */
23 package com.owncloud.android.ui.activity;
24
25 import java.util.Arrays;
26
27 import android.content.SharedPreferences;
28 import android.os.Bundle;
29 import android.preference.PreferenceManager;
30 import android.text.Editable;
31 import android.text.TextWatcher;
32 import android.view.KeyEvent;
33 import android.view.View;
34 import android.view.View.OnClickListener;
35 import android.widget.Button;
36 import android.widget.EditText;
37 import android.widget.TextView;
38 import android.widget.Toast;
39
40 import com.actionbarsherlock.app.ActionBar;
41 import com.actionbarsherlock.app.SherlockFragmentActivity;
42 import com.owncloud.android.R;
43 import com.owncloud.android.lib.common.utils.Log_OC;
44 import com.owncloud.android.utils.DisplayUtils;
45
46 public class PassCodeActivity extends SherlockFragmentActivity {
47
48
49 private static final String TAG = PassCodeActivity.class.getSimpleName();
50
51 public final static String ACTION_ENABLE = PassCodeActivity.class.getCanonicalName() + ".ENABLE";
52 public final static String ACTION_DISABLE = PassCodeActivity.class.getCanonicalName() + ".DISABLE";
53 public final static String ACTION_REQUEST = PassCodeActivity.class.getCanonicalName() + ".REQUEST";
54
55 private Button mBCancel;
56 private TextView mPassCodeHdr;
57 private TextView mPassCodeHdrExplanation;
58 private EditText[] mPassCodeEditTexts = new EditText[4];
59
60 private String [] mPassCodeDigits = {"","","",""};
61 private boolean mConfirmingPassCode = false;
62
63 private boolean mBChange = true; // to control that only one blocks jump
64
65
66 /**
67 * Initializes the activity.
68 *
69 * An intent with a valid ACTION is expected; if none is found, an {@link IllegalArgumentException} will be thrown.
70 *
71 * @param savedInstanceState Previously saved state - irrelevant in this case
72 */
73 protected void onCreate(Bundle savedInstanceState) {
74 super.onCreate(savedInstanceState);
75 setContentView(R.layout.passcodelock);
76
77 mBCancel = (Button) findViewById(R.id.cancel);
78 mPassCodeHdr = (TextView) findViewById(R.id.header);
79 mPassCodeHdrExplanation = (TextView) findViewById(R.id.explanation);
80 mPassCodeEditTexts[0] = (EditText) findViewById(R.id.txt0);
81 mPassCodeEditTexts[0].requestFocus();
82 getWindow().setSoftInputMode(android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
83 mPassCodeEditTexts[1] = (EditText) findViewById(R.id.txt1);
84 mPassCodeEditTexts[2] = (EditText) findViewById(R.id.txt2);
85 mPassCodeEditTexts[3] = (EditText) findViewById(R.id.txt3);
86
87 if (ACTION_REQUEST.equals(getIntent().getAction())) {
88 /// this is a pass code request; the user has to input the right value
89 mPassCodeHdr.setText(R.string.pass_code_enter_pass_code);
90 mPassCodeHdrExplanation.setVisibility(View.INVISIBLE);
91 setCancelButtonEnabled(false); // no option to cancel
92
93 } else if (ACTION_ENABLE.equals(getIntent().getAction())) {
94 /// pass code preference has just been activated in Preferences; will receive and confirm pass code value
95 mPassCodeHdr.setText(R.string.pass_code_configure_your_pass_code);
96 //mPassCodeHdr.setText(R.string.pass_code_enter_pass_code); // TODO choose a header, check iOS
97 mPassCodeHdrExplanation.setVisibility(View.VISIBLE);
98 setCancelButtonEnabled(true);
99
100 } else if (ACTION_DISABLE.equals(getIntent().getAction())) {
101 /// pass code preference has just been disabled in Preferences;
102 // will confirm user knows pass code, then remove it
103 mPassCodeHdr.setText(R.string.pass_code_remove_your_pass_code);
104 mPassCodeHdrExplanation.setVisibility(View.INVISIBLE);
105 setCancelButtonEnabled(true);
106
107 } else {
108 throw new IllegalArgumentException("A valid ACTION is needed in the Intent passed to " + TAG);
109 }
110
111 setTextListeners();
112
113 ActionBar actionBar = getSupportActionBar();
114 actionBar.setIcon(DisplayUtils.getSeasonalIconId());
115 }
116
117
118 /**
119 * Enables or disables the cancel button to allow the user interrupt the ACTION requested to the activity.
120 *
121 * @param enabled 'True' makes the cancel button available, 'false' hides it.
122 */
123 protected void setCancelButtonEnabled(boolean enabled){
124 if(enabled){
125 mBCancel.setVisibility(View.VISIBLE);
126 mBCancel.setOnClickListener(new OnClickListener() {
127 @Override
128 public void onClick(View v) {
129 revertActionAndExit();
130 }
131 });
132 } else {
133 mBCancel.setVisibility(View.GONE);
134 mBCancel.setVisibility(View.INVISIBLE);
135 mBCancel.setOnClickListener(null);
136 }
137 }
138
139
140 /**
141 * Binds the appropiate listeners to the input boxes receiving each digit of the pass code.
142 */
143 protected void setTextListeners() {
144
145 /// First input field
146 mPassCodeEditTexts[0].addTextChangedListener(new PassCodeDigitTextWatcher(0, false));
147
148
149 /*------------------------------------------------
150 * SECOND BOX
151 -------------------------------------------------*/
152 mPassCodeEditTexts[1].addTextChangedListener(new PassCodeDigitTextWatcher(1, false));
153
154 mPassCodeEditTexts[1].setOnKeyListener(new View.OnKeyListener() {
155
156 @Override
157 public boolean onKey(View v, int keyCode, KeyEvent event) {
158 if (keyCode == KeyEvent.KEYCODE_DEL && mBChange) { // TODO WIP: event should be used to control what's exactly happening with DEL, not any custom field...
159 mPassCodeEditTexts[0].setText("");
160 mPassCodeEditTexts[0].requestFocus();
161 if (!mConfirmingPassCode)
162 mPassCodeDigits[0] = "";
163 mBChange = false;
164
165 } else if (!mBChange) {
166 mBChange = true;
167 }
168 return false;
169 }
170 });
171
172 mPassCodeEditTexts[1].setOnFocusChangeListener(new View.OnFocusChangeListener() {
173
174 @Override
175 public void onFocusChange(View v, boolean hasFocus) {
176 /// TODO WIP: should take advantage of hasFocus to reduce processing
177 mPassCodeEditTexts[1].setCursorVisible(true); // TODO WIP this could be made static, or just nothing, since default is true
178 if (mPassCodeEditTexts[0].getText().toString().equals("")) { // TODO WIP validation could be done in a global way, with a single OnFocusChangeListener for all the input fields
179 mPassCodeEditTexts[1].setSelected(false);
180 mPassCodeEditTexts[1].setCursorVisible(false); // TODO WIP don't think we really need to enable and disable it; fields without focus should not show it
181 mPassCodeEditTexts[0].requestFocus();
182 mPassCodeEditTexts[0].setSelected(true); // TODO WIP really needed? is it for the colour highlight
183 mPassCodeEditTexts[0].setSelection(0); // TODO WIP what is THIS for?
184 }
185
186 }
187 });
188
189
190 /*------------------------------------------------
191 * THIRD BOX
192 -------------------------------------------------*/
193 mPassCodeEditTexts[2].addTextChangedListener(new PassCodeDigitTextWatcher(2, false));
194
195 mPassCodeEditTexts[2].setOnKeyListener(new View.OnKeyListener() {
196
197 @Override
198 public boolean onKey(View v, int keyCode, KeyEvent event) {
199 if (keyCode == KeyEvent.KEYCODE_DEL && mBChange) {
200 mPassCodeEditTexts[1].requestFocus();
201 if (!mConfirmingPassCode)
202 mPassCodeDigits[1] = "";
203 mPassCodeEditTexts[1].setText("");
204 mBChange = false;
205
206 } else if (!mBChange) {
207 mBChange = true;
208
209 }
210 return false;
211 }
212 });
213
214 mPassCodeEditTexts[2].setOnFocusChangeListener(new View.OnFocusChangeListener() {
215
216 @Override
217 public void onFocusChange(View v, boolean hasFocus) {
218 mPassCodeEditTexts[2].setCursorVisible(true);
219 if (mPassCodeEditTexts[0].getText().toString().equals("")) {
220 mPassCodeEditTexts[2].setSelected(false);
221 mPassCodeEditTexts[2].setCursorVisible(false);
222 mPassCodeEditTexts[0].requestFocus();
223 mPassCodeEditTexts[0].setSelected(true);
224 mPassCodeEditTexts[0].setSelection(0);
225 } else if (mPassCodeEditTexts[1].getText().toString().equals("")) {
226 mPassCodeEditTexts[2].setSelected(false);
227 mPassCodeEditTexts[2].setCursorVisible(false);
228 mPassCodeEditTexts[1].requestFocus();
229 mPassCodeEditTexts[1].setSelected(true);
230 mPassCodeEditTexts[1].setSelection(0);
231 }
232
233 }
234 });
235
236
237 /*------------------------------------------------
238 * FOURTH BOX
239 -------------------------------------------------*/
240 mPassCodeEditTexts[3].addTextChangedListener(new PassCodeDigitTextWatcher(3, true));
241
242 mPassCodeEditTexts[3].setOnKeyListener(new View.OnKeyListener() {
243
244 @Override
245 public boolean onKey(View v, int keyCode, KeyEvent event) {
246 if (keyCode == KeyEvent.KEYCODE_DEL && mBChange) {
247 mPassCodeEditTexts[2].requestFocus();
248 if (!mConfirmingPassCode)
249 mPassCodeDigits[2] = "";
250 mPassCodeEditTexts[2].setText("");
251 mBChange = false;
252
253 } else if (!mBChange) {
254 mBChange = true;
255 }
256 return false;
257 }
258 });
259
260 mPassCodeEditTexts[3].setOnFocusChangeListener(new View.OnFocusChangeListener() {
261
262 @Override
263 public void onFocusChange(View v, boolean hasFocus) {
264 mPassCodeEditTexts[3]. setCursorVisible(true);
265
266 if (mPassCodeEditTexts[0].getText().toString().equals("")) {
267 mPassCodeEditTexts[3].setSelected(false);
268 mPassCodeEditTexts[3].setCursorVisible(false);
269 mPassCodeEditTexts[0].requestFocus();
270 mPassCodeEditTexts[0].setSelected(true);
271 mPassCodeEditTexts[0].setSelection(0);
272 } else if (mPassCodeEditTexts[1].getText().toString().equals("")) {
273 mPassCodeEditTexts[3].setSelected(false);
274 mPassCodeEditTexts[3].setCursorVisible(false);
275 mPassCodeEditTexts[1].requestFocus();
276 mPassCodeEditTexts[1].setSelected(true);
277 mPassCodeEditTexts[1].setSelection(0);
278 } else if (mPassCodeEditTexts[2].getText().toString().equals("")) {
279 mPassCodeEditTexts[3].setSelected(false);
280 mPassCodeEditTexts[3].setCursorVisible(false);
281 mPassCodeEditTexts[2].requestFocus();
282 mPassCodeEditTexts[2].setSelected(true);
283 mPassCodeEditTexts[2].setSelection(0);
284 }
285
286 }
287 });
288
289 } // end setTextListener
290
291
292 /**
293 * Processes the pass code entered by the user just after the last digit was in.
294 *
295 * Takes into account the action requested to the activity, the currently saved pass code and the previously
296 * typed pass code, if any.
297 */
298 private void processFullPassCode() {
299 if (ACTION_REQUEST.equals(getIntent().getAction())) {
300 if (checkPassCode()) {
301 /// pass code accepted in request, user is allowed to access the app
302 finish();
303
304 } else {
305 showErrorAndRestart(R.string.common_error, R.string.pass_code_enter_pass_code, View.INVISIBLE);
306 /// TODO better error message
307 }
308
309 } else if (ACTION_DISABLE.equals(getIntent().getAction())) {
310 if (checkPassCode()) {
311 /// pass code accepted when disabling, pass code is removed
312 SharedPreferences.Editor appPrefs = PreferenceManager
313 .getDefaultSharedPreferences(getApplicationContext()).edit();
314 appPrefs.putBoolean("set_pincode", false); // TODO remove; this should be unnecessary, was done before entering in the activity
315 appPrefs.commit();
316
317 Toast.makeText(PassCodeActivity.this, R.string.pass_code_removed, Toast.LENGTH_LONG).show();
318 finish();
319
320 } else {
321 showErrorAndRestart(R.string.common_error, R.string.pass_code_enter_pass_code, View.INVISIBLE);
322 /// TODO better error message
323 }
324
325 } else if (ACTION_ENABLE.equals(getIntent().getAction())) {
326 /// enabling pass code
327 if (!mConfirmingPassCode) {
328 requestPassCodeConfirmation();
329
330 } else if (confirmPassCode()) {
331 /// confirmed: user typed the same pass code twice
332 savePassCodeAndExit();
333
334 } else {
335 showErrorAndRestart(
336 R.string.pass_code_mismatch, R.string.pass_code_configure_your_pass_code, View.VISIBLE
337 );
338 }
339 }
340 }
341
342
343 private void showErrorAndRestart(int errorMessage, int headerMessage, int explanationVisibility) {
344 Arrays.fill(mPassCodeDigits, null);
345 CharSequence errorSeq = getString(errorMessage);
346 Toast.makeText(this, errorSeq, Toast.LENGTH_LONG).show();
347 mPassCodeHdr.setText(headerMessage); // TODO check if really needed
348 mPassCodeHdrExplanation.setVisibility(explanationVisibility); // TODO check if really needed
349 clearBoxes();
350 }
351
352
353 /**
354 * Ask to the user for retyping the pass code just entered before saving it as the current pass code.
355 */
356 protected void requestPassCodeConfirmation(){
357 clearBoxes();
358 mPassCodeHdr.setText(R.string.pass_code_reenter_your_pass_code);
359 mPassCodeHdrExplanation.setVisibility(View.INVISIBLE);
360 mConfirmingPassCode = true;
361 }
362
363 /**
364 * Compares pass code entered by the user with the value currently saved in the app.
365 *
366 * @return 'True' if entered pass code equals to the saved one.
367 */
368 protected boolean checkPassCode(){
369 SharedPreferences appPrefs = PreferenceManager
370 .getDefaultSharedPreferences(getApplicationContext());
371
372 String savedPassCodeDigits[] = new String[4];
373 savedPassCodeDigits[0] = appPrefs.getString("PrefPinCode1", null);
374 savedPassCodeDigits[1] = appPrefs.getString("PrefPinCode2", null);
375 savedPassCodeDigits[2] = appPrefs.getString("PrefPinCode3", null);
376 savedPassCodeDigits[3] = appPrefs.getString("PrefPinCode4", null);
377
378 boolean result = true;
379 for (int i = 0; i < mPassCodeDigits.length && result; i++) {
380 result = result && (mPassCodeDigits[i] != null) && mPassCodeDigits[i].equals(savedPassCodeDigits[i]);
381 }
382 return result;
383 }
384
385 /**
386 * Compares pass code retyped by the user in the input fields with the value entered just before.
387 *
388 * @return 'True' if retyped pass code equals to the entered before.
389 */
390 protected boolean confirmPassCode(){
391 mConfirmingPassCode = false;
392
393 boolean result = true;
394 for (int i = 0; i < mPassCodeEditTexts.length && result; i++) {
395 result = result && ((mPassCodeEditTexts[i].getText().toString()).equals(mPassCodeDigits[i]));
396 }
397 return result;
398 }
399
400 /**
401 * Sets the input fields to empty strings and puts the focus on the first one.
402 */
403 protected void clearBoxes(){
404 for (int i=0; i < mPassCodeEditTexts.length; i++) {
405 mPassCodeEditTexts[i].setText("");
406 }
407 mPassCodeEditTexts[0].requestFocus();
408 }
409
410 /**
411 * Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while preventing
412 * than ACTION_REQUEST may be worked around.
413 *
414 * @param keyCode Key code of the key that triggered the down event.
415 * @param event Event triggered.
416 * @return 'True' when the key event was processed by this method.
417 */
418 @Override
419 public boolean onKeyDown(int keyCode, KeyEvent event){
420 if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount()== 0){
421 if (ACTION_ENABLE.equals(getIntent().getAction()) || ACTION_DISABLE.equals(getIntent().getAction())) {
422 revertActionAndExit();
423 }
424 return true;
425 }
426 return super.onKeyDown(keyCode, event);
427 }
428
429 /**
430 * Saves the pass code input by the user as the current pass code.
431 */
432 protected void savePassCodeAndExit() {
433 SharedPreferences.Editor appPrefs = PreferenceManager
434 .getDefaultSharedPreferences(getApplicationContext()).edit();
435
436 appPrefs.putString("PrefPinCode1", mPassCodeDigits[0]);
437 appPrefs.putString("PrefPinCode2", mPassCodeDigits[1]);
438 appPrefs.putString("PrefPinCode3", mPassCodeDigits[2]);
439 appPrefs.putString("PrefPinCode4", mPassCodeDigits[3]);
440 appPrefs.putBoolean("set_pincode", true); /// TODO remove; unnecessary, Preferences did it before entering here
441 appPrefs.commit();
442
443 Toast.makeText(this, R.string.pass_code_stored, Toast.LENGTH_LONG).show();
444 finish();
445 }
446
447 /**
448 * Cancellation of ACTION_ENABLE or ACTION_DISABLE; reverts the enable or disable action done by
449 * {@link Preferences}, then finishes.
450 */
451 protected void revertActionAndExit() {
452 SharedPreferences.Editor appPrefsE = PreferenceManager
453 .getDefaultSharedPreferences(getApplicationContext()).edit();
454
455 SharedPreferences appPrefs = PreferenceManager
456 .getDefaultSharedPreferences(getApplicationContext());
457
458 boolean state = appPrefs.getBoolean("set_pincode", false);
459 appPrefsE.putBoolean("set_pincode", !state);
460 // TODO WIP: this is reverting the value of the preference because it was changed BEFORE entering
461 // TODO in this activity; was the PreferenceCheckBox in the caller who did it
462 appPrefsE.commit();
463 finish();
464 }
465
466
467 private class PassCodeDigitTextWatcher implements TextWatcher {
468
469 private int mIndex = -1;
470 private boolean mLastOne = false;
471
472 /**
473 * Constructor
474 *
475 * @param index Position in the pass code of the input field that will be bound to this watcher.
476 * @param lastOne 'True' means that watcher corresponds to the last position of the pass code.
477 */
478 public PassCodeDigitTextWatcher(int index, boolean lastOne) {
479 mIndex = index;
480 mLastOne = lastOne;
481 if (mIndex < 0) {
482 throw new IllegalArgumentException(
483 "Invalid index in " + PassCodeDigitTextWatcher.class.getSimpleName() + " constructor"
484 );
485 }
486 }
487
488 private int next() {
489 return mLastOne ? 0 : mIndex + 1;
490 }
491
492 /**
493 * Performs several actions when the user types a digit in an input field:
494 * - saves the input digit to the state of the activity; this will allow retyping the pass code to confirm it.
495 * - moves the focus automatically to the next field
496 * - for the last field, triggers the processing of the full pass code
497 *
498 * @param s
499 */
500 @Override
501 public void afterTextChanged(Editable s) {
502 if (s.length() > 0) {
503 if (!mConfirmingPassCode) {
504 mPassCodeDigits[mIndex] = mPassCodeEditTexts[mIndex].getText().toString();
505 }
506 mPassCodeEditTexts[next()].requestFocus();
507
508 if (mLastOne) {
509 processFullPassCode();
510 }
511
512 } else {
513 Log_OC.d(TAG, "Text box " + mIndex + " was cleaned");
514 }
515 }
516
517 @Override
518 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
519 // nothing to do
520 }
521
522 @Override
523 public void onTextChanged(CharSequence s, int start, int before, int count) {
524 // nothing to do
525 }
526
527 }
528
529
530 }