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