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