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