Merge remote-tracking branch 'remotes/upstream/master' into beta
[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.Intent;
28 import android.content.SharedPreferences;
29 import android.os.Bundle;
30 import android.preference.PreferenceManager;
31 import android.support.v7.app.AppCompatActivity;
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
45 public class PassCodeActivity extends AppCompatActivity {
46
47 private static final String TAG = PassCodeActivity.class.getSimpleName();
48
49 public final static String ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT";
50 public final static String ACTION_CHECK_WITH_RESULT = "ACTION_CHECK_WITH_RESULT";
51 public final static String ACTION_CHECK = "ACTION_CHECK";
52
53 public final static String KEY_PASSCODE = "KEY_PASSCODE";
54 public final static String KEY_CHECK_RESULT = "KEY_CHECK_RESULT";
55
56 private Button mBCancel;
57 private TextView mPassCodeHdr;
58 private TextView mPassCodeHdrExplanation;
59 private EditText[] mPassCodeEditTexts = new EditText[4];
60
61 private String [] mPassCodeDigits = {"","","",""};
62 private static String KEY_PASSCODE_DIGITS = "PASSCODE_DIGITS";
63 private boolean mConfirmingPassCode = false;
64 private static String KEY_CONFIRMING_PASSCODE = "CONFIRMING_PASSCODE";
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_CHECK.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_REQUEST_WITH_RESULT.equals(getIntent().getAction())) {
99 if (savedInstanceState != null) {
100 mConfirmingPassCode = savedInstanceState.getBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE);
101 mPassCodeDigits = savedInstanceState.getStringArray(PassCodeActivity.KEY_PASSCODE_DIGITS);
102 }
103 if(mConfirmingPassCode){
104 //the app was in the passcodeconfirmation
105 requestPassCodeConfirmation();
106 }else{
107 /// pass code preference has just been activated in Preferences;
108 // will receive and confirm pass code value
109 mPassCodeHdr.setText(R.string.pass_code_configure_your_pass_code);
110 //mPassCodeHdr.setText(R.string.pass_code_enter_pass_code);
111 // TODO choose a header, check iOS
112 mPassCodeHdrExplanation.setVisibility(View.VISIBLE);
113 setCancelButtonEnabled(true);
114 }
115
116 } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
117 /// pass code preference has just been disabled in Preferences;
118 // will confirm user knows pass code, then remove it
119 mPassCodeHdr.setText(R.string.pass_code_remove_your_pass_code);
120 mPassCodeHdrExplanation.setVisibility(View.INVISIBLE);
121 setCancelButtonEnabled(true);
122
123 } else {
124 throw new IllegalArgumentException("A valid ACTION is needed in the Intent passed to "
125 + TAG);
126 }
127
128 setTextListeners();
129 }
130
131
132 /**
133 * Enables or disables the cancel button to allow the user interrupt the ACTION
134 * requested to the activity.
135 *
136 * @param enabled 'True' makes the cancel button available, 'false' hides it.
137 */
138 protected void setCancelButtonEnabled(boolean enabled){
139 if(enabled){
140 mBCancel.setVisibility(View.VISIBLE);
141 mBCancel.setOnClickListener(new OnClickListener() {
142 @Override
143 public void onClick(View v) {
144 revertActionAndExit();
145 }
146 });
147 } else {
148 mBCancel.setVisibility(View.GONE);
149 mBCancel.setVisibility(View.INVISIBLE);
150 mBCancel.setOnClickListener(null);
151 }
152 }
153
154
155 /**
156 * Binds the appropiate listeners to the input boxes receiving each digit of the pass code.
157 */
158 protected void setTextListeners() {
159
160 /// First input field
161 mPassCodeEditTexts[0].addTextChangedListener(new PassCodeDigitTextWatcher(0, false));
162
163
164 /*------------------------------------------------
165 * SECOND BOX
166 -------------------------------------------------*/
167 mPassCodeEditTexts[1].addTextChangedListener(new PassCodeDigitTextWatcher(1, false));
168
169 mPassCodeEditTexts[1].setOnKeyListener(new View.OnKeyListener() {
170
171 @Override
172 public boolean onKey(View v, int keyCode, KeyEvent event) {
173 if (keyCode == KeyEvent.KEYCODE_DEL && mBChange) { // TODO WIP: event should be
174 // used to control what's exactly happening with DEL, not any custom field...
175 mPassCodeEditTexts[0].setText("");
176 mPassCodeEditTexts[0].requestFocus();
177 if (!mConfirmingPassCode)
178 mPassCodeDigits[0] = "";
179 mBChange = false;
180
181 } else if (!mBChange) {
182 mBChange = true;
183 }
184 return false;
185 }
186 });
187
188 mPassCodeEditTexts[1].setOnFocusChangeListener(new View.OnFocusChangeListener() {
189
190 @Override
191 public void onFocusChange(View v, boolean hasFocus) {
192 /// TODO WIP: should take advantage of hasFocus to reduce processing
193 if (mPassCodeEditTexts[0].getText().toString().equals("")) { // TODO WIP validation
194 // could be done in a global way, with a single OnFocusChangeListener for all the
195 // input fields
196 mPassCodeEditTexts[0].requestFocus();
197 }
198 }
199 });
200
201
202 /*------------------------------------------------
203 * THIRD BOX
204 -------------------------------------------------*/
205 mPassCodeEditTexts[2].addTextChangedListener(new PassCodeDigitTextWatcher(2, false));
206
207 mPassCodeEditTexts[2].setOnKeyListener(new View.OnKeyListener() {
208
209 @Override
210 public boolean onKey(View v, int keyCode, KeyEvent event) {
211 if (keyCode == KeyEvent.KEYCODE_DEL && mBChange) {
212 mPassCodeEditTexts[1].requestFocus();
213 if (!mConfirmingPassCode)
214 mPassCodeDigits[1] = "";
215 mPassCodeEditTexts[1].setText("");
216 mBChange = false;
217
218 } else if (!mBChange) {
219 mBChange = true;
220
221 }
222 return false;
223 }
224 });
225
226 mPassCodeEditTexts[2].setOnFocusChangeListener(new View.OnFocusChangeListener() {
227
228 @Override
229 public void onFocusChange(View v, boolean hasFocus) {
230 if (mPassCodeEditTexts[0].getText().toString().equals("")) {
231 mPassCodeEditTexts[0].requestFocus();
232 } else if (mPassCodeEditTexts[1].getText().toString().equals("")) {
233 mPassCodeEditTexts[1].requestFocus();
234 }
235 }
236 });
237
238
239 /*------------------------------------------------
240 * FOURTH BOX
241 -------------------------------------------------*/
242 mPassCodeEditTexts[3].addTextChangedListener(new PassCodeDigitTextWatcher(3, true));
243
244 mPassCodeEditTexts[3].setOnKeyListener(new View.OnKeyListener() {
245
246 @Override
247 public boolean onKey(View v, int keyCode, KeyEvent event) {
248 if (keyCode == KeyEvent.KEYCODE_DEL && mBChange) {
249 mPassCodeEditTexts[2].requestFocus();
250 if (!mConfirmingPassCode)
251 mPassCodeDigits[2] = "";
252 mPassCodeEditTexts[2].setText("");
253 mBChange = false;
254
255 } else if (!mBChange) {
256 mBChange = true;
257 }
258 return false;
259 }
260 });
261
262 mPassCodeEditTexts[3].setOnFocusChangeListener(new View.OnFocusChangeListener() {
263
264 @Override
265 public void onFocusChange(View v, boolean hasFocus) {
266
267 if (mPassCodeEditTexts[0].getText().toString().equals("")) {
268 mPassCodeEditTexts[0].requestFocus();
269 } else if (mPassCodeEditTexts[1].getText().toString().equals("")) {
270 mPassCodeEditTexts[1].requestFocus();
271 } else if (mPassCodeEditTexts[2].getText().toString().equals("")) {
272 mPassCodeEditTexts[2].requestFocus();
273 }
274
275 }
276 });
277
278 } // end setTextListener
279
280
281 /**
282 * Processes the pass code entered by the user just after the last digit was in.
283 *
284 * Takes into account the action requested to the activity, the currently saved pass code and
285 * the previously typed pass code, if any.
286 */
287 private void processFullPassCode() {
288 if (ACTION_CHECK.equals(getIntent().getAction())) {
289 if (checkPassCode()) {
290 /// pass code accepted in request, user is allowed to access the app
291 finish();
292
293 } else {
294 showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code,
295 View.INVISIBLE);
296 }
297
298 } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
299 if (checkPassCode()) {
300
301 Intent resultIntent = new Intent();
302 resultIntent.putExtra(KEY_CHECK_RESULT, true);
303 setResult(RESULT_OK, resultIntent);
304
305 finish();
306 } else {
307 showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code,
308 View.INVISIBLE);
309 }
310
311 } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) {
312 /// enabling pass code
313 if (!mConfirmingPassCode) {
314 requestPassCodeConfirmation();
315
316 } else if (confirmPassCode()) {
317 /// confirmed: user typed the same pass code twice
318 savePassCodeAndExit();
319
320 } else {
321 showErrorAndRestart(
322 R.string.pass_code_mismatch, R.string.pass_code_configure_your_pass_code, View.VISIBLE
323 );
324 }
325 }
326 }
327
328
329 private void showErrorAndRestart(int errorMessage, int headerMessage,
330 int explanationVisibility) {
331 Arrays.fill(mPassCodeDigits, null);
332 CharSequence errorSeq = getString(errorMessage);
333 Toast.makeText(this, errorSeq, Toast.LENGTH_LONG).show();
334 mPassCodeHdr.setText(headerMessage); // TODO check if really needed
335 mPassCodeHdrExplanation.setVisibility(explanationVisibility); // TODO check if really needed
336 clearBoxes();
337 }
338
339
340 /**
341 * Ask to the user for retyping the pass code just entered before saving it as the current pass
342 * code.
343 */
344 protected void requestPassCodeConfirmation(){
345 clearBoxes();
346 mPassCodeHdr.setText(R.string.pass_code_reenter_your_pass_code);
347 mPassCodeHdrExplanation.setVisibility(View.INVISIBLE);
348 mConfirmingPassCode = true;
349 }
350
351 /**
352 * Compares pass code entered by the user with the value currently saved in the app.
353 *
354 * @return 'True' if entered pass code equals to the saved one.
355 */
356 protected boolean checkPassCode(){
357 SharedPreferences appPrefs = PreferenceManager
358 .getDefaultSharedPreferences(getApplicationContext());
359
360 String savedPassCodeDigits[] = new String[4];
361 savedPassCodeDigits[0] = appPrefs.getString("PrefPinCode1", null);
362 savedPassCodeDigits[1] = appPrefs.getString("PrefPinCode2", null);
363 savedPassCodeDigits[2] = appPrefs.getString("PrefPinCode3", null);
364 savedPassCodeDigits[3] = appPrefs.getString("PrefPinCode4", null);
365
366 boolean result = true;
367 for (int i = 0; i < mPassCodeDigits.length && result; i++) {
368 result = result && (mPassCodeDigits[i] != null) &&
369 mPassCodeDigits[i].equals(savedPassCodeDigits[i]);
370 }
371 return result;
372 }
373
374 /**
375 * Compares pass code retyped by the user in the input fields with the value entered just
376 * before.
377 *
378 * @return 'True' if retyped pass code equals to the entered before.
379 */
380 protected boolean confirmPassCode(){
381 mConfirmingPassCode = false;
382
383 boolean result = true;
384 for (int i = 0; i < mPassCodeEditTexts.length && result; i++) {
385 result = result &&
386 ((mPassCodeEditTexts[i].getText().toString()).equals(mPassCodeDigits[i]));
387 }
388 return result;
389 }
390
391 /**
392 * Sets the input fields to empty strings and puts the focus on the first one.
393 */
394 protected void clearBoxes(){
395 for (int i=0; i < mPassCodeEditTexts.length; i++) {
396 mPassCodeEditTexts[i].setText("");
397 }
398 mPassCodeEditTexts[0].requestFocus();
399 }
400
401 /**
402 * Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while
403 * preventing than ACTION_CHECK may be worked around.
404 *
405 * @param keyCode Key code of the key that triggered the down event.
406 * @param event Event triggered.
407 * @return 'True' when the key event was processed by this method.
408 */
409 @Override
410 public boolean onKeyDown(int keyCode, KeyEvent event){
411 if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount()== 0){
412 if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction()) ||
413 ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
414 revertActionAndExit();
415 }
416 return true;
417 }
418 return super.onKeyDown(keyCode, event);
419 }
420
421 /**
422 * Saves the pass code input by the user as the current pass code.
423 */
424 protected void savePassCodeAndExit() {
425 SharedPreferences.Editor appPrefs = PreferenceManager
426 .getDefaultSharedPreferences(getApplicationContext()).edit();
427
428 Intent resultIntent = new Intent();
429 resultIntent.putExtra(KEY_PASSCODE,
430 mPassCodeDigits[0] + mPassCodeDigits[1] + mPassCodeDigits[2] + mPassCodeDigits[3]);
431
432 setResult(RESULT_OK, resultIntent);
433 finish();
434 }
435
436 /**
437 * Cancellation of ACTION_ENABLE or ACTION_DISABLE; reverts the enable or disable action done by
438 * {@link Preferences}, then finishes.
439 */
440 protected void revertActionAndExit() {
441 SharedPreferences.Editor appPrefsE = PreferenceManager
442 .getDefaultSharedPreferences(getApplicationContext()).edit();
443
444 SharedPreferences appPrefs = PreferenceManager
445 .getDefaultSharedPreferences(getApplicationContext());
446
447 boolean state = appPrefs.getBoolean("set_pincode", false);
448 appPrefsE.putBoolean("set_pincode", !state);
449 // TODO WIP: this is reverting the value of the preference because it was changed BEFORE
450 // entering
451 // TODO in this activity; was the PreferenceCheckBox in the caller who did it
452 appPrefsE.commit();
453 finish();
454 }
455
456 @Override
457 public void onSaveInstanceState(Bundle outState) {
458 super.onSaveInstanceState(outState);
459 outState.putBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE, mConfirmingPassCode);
460 outState.putStringArray(PassCodeActivity.KEY_PASSCODE_DIGITS, mPassCodeDigits);
461 }
462
463 private class PassCodeDigitTextWatcher implements TextWatcher {
464
465 private int mIndex = -1;
466 private boolean mLastOne = false;
467
468 /**
469 * Constructor
470 *
471 * @param index Position in the pass code of the input field that will be bound to
472 * this watcher.
473 * @param lastOne 'True' means that watcher corresponds to the last position of the
474 * pass code.
475 */
476 public PassCodeDigitTextWatcher(int index, boolean lastOne) {
477 mIndex = index;
478 mLastOne = lastOne;
479 if (mIndex < 0) {
480 throw new IllegalArgumentException(
481 "Invalid index in " + PassCodeDigitTextWatcher.class.getSimpleName() +
482 " constructor"
483 );
484 }
485 }
486
487 private int next() {
488 return mLastOne ? 0 : mIndex + 1;
489 }
490
491 /**
492 * Performs several actions when the user types a digit in an input field:
493 * - saves the input digit to the state of the activity; this will allow retyping the
494 * 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 }