2 * ownCloud Android client application
4 * @author Bartek Przybylski
6 * @author David A. Velasco
7 * Copyright (C) 2011 Bartek Przybylski
8 * Copyright (C) 2015 ownCloud Inc.
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.
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.
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/>.
23 package com
.owncloud
.android
.ui
.activity
;
25 import java
.util
.Arrays
;
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
;
42 import com
.owncloud
.android
.R
;
43 import com
.owncloud
.android
.lib
.common
.utils
.Log_OC
;
45 public class PassCodeActivity
extends AppCompatActivity
{
47 private static final String TAG
= PassCodeActivity
.class.getSimpleName();
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";
53 public final static String KEY_PASSCODE
= "KEY_PASSCODE";
54 public final static String KEY_CHECK_RESULT
= "KEY_CHECK_RESULT";
56 private Button mBCancel
;
57 private TextView mPassCodeHdr
;
58 private TextView mPassCodeHdrExplanation
;
59 private EditText
[] mPassCodeEditTexts
= new EditText
[4];
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";
66 private boolean mBChange
= true
; // to control that only one blocks jump
70 * Initializes the activity.
72 * An intent with a valid ACTION is expected; if none is found, an
73 * {@link IllegalArgumentException} will be thrown.
75 * @param savedInstanceState Previously saved state - irrelevant in this case
77 protected void onCreate(Bundle savedInstanceState
) {
78 super.onCreate(savedInstanceState
);
79 setContentView(R
.layout
.passcodelock
);
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
);
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
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
);
103 if(mConfirmingPassCode
){
104 //the app was in the passcodeconfirmation
105 requestPassCodeConfirmation();
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
);
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
);
124 throw new IllegalArgumentException("A valid ACTION is needed in the Intent passed to "
133 * Enables or disables the cancel button to allow the user interrupt the ACTION
134 * requested to the activity.
136 * @param enabled 'True' makes the cancel button available, 'false' hides it.
138 protected void setCancelButtonEnabled(boolean enabled
){
140 mBCancel
.setVisibility(View
.VISIBLE
);
141 mBCancel
.setOnClickListener(new OnClickListener() {
143 public void onClick(View v
) {
144 revertActionAndExit();
148 mBCancel
.setVisibility(View
.GONE
);
149 mBCancel
.setVisibility(View
.INVISIBLE
);
150 mBCancel
.setOnClickListener(null
);
156 * Binds the appropiate listeners to the input boxes receiving each digit of the pass code.
158 protected void setTextListeners() {
160 /// First input field
161 mPassCodeEditTexts
[0].addTextChangedListener(new PassCodeDigitTextWatcher(0, false
));
164 /*------------------------------------------------
166 -------------------------------------------------*/
167 mPassCodeEditTexts
[1].addTextChangedListener(new PassCodeDigitTextWatcher(1, false
));
169 mPassCodeEditTexts
[1].setOnKeyListener(new View
.OnKeyListener() {
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] = "";
181 } else if (!mBChange
) {
188 mPassCodeEditTexts
[1].setOnFocusChangeListener(new View
.OnFocusChangeListener() {
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
196 mPassCodeEditTexts
[0].requestFocus();
202 /*------------------------------------------------
204 -------------------------------------------------*/
205 mPassCodeEditTexts
[2].addTextChangedListener(new PassCodeDigitTextWatcher(2, false
));
207 mPassCodeEditTexts
[2].setOnKeyListener(new View
.OnKeyListener() {
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("");
218 } else if (!mBChange
) {
226 mPassCodeEditTexts
[2].setOnFocusChangeListener(new View
.OnFocusChangeListener() {
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();
239 /*------------------------------------------------
241 -------------------------------------------------*/
242 mPassCodeEditTexts
[3].addTextChangedListener(new PassCodeDigitTextWatcher(3, true
));
244 mPassCodeEditTexts
[3].setOnKeyListener(new View
.OnKeyListener() {
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("");
255 } else if (!mBChange
) {
262 mPassCodeEditTexts
[3].setOnFocusChangeListener(new View
.OnFocusChangeListener() {
265 public void onFocusChange(View v
, boolean hasFocus
) {
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();
278 } // end setTextListener
282 * Processes the pass code entered by the user just after the last digit was in.
284 * Takes into account the action requested to the activity, the currently saved pass code and
285 * the previously typed pass code, if any.
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
294 showErrorAndRestart(R
.string
.pass_code_wrong
, R
.string
.pass_code_enter_pass_code
,
298 } else if (ACTION_CHECK_WITH_RESULT
.equals(getIntent().getAction())) {
299 if (checkPassCode()) {
301 Intent resultIntent
= new Intent();
302 resultIntent
.putExtra(KEY_CHECK_RESULT
, true
);
303 setResult(RESULT_OK
, resultIntent
);
307 showErrorAndRestart(R
.string
.pass_code_wrong
, R
.string
.pass_code_enter_pass_code
,
311 } else if (ACTION_REQUEST_WITH_RESULT
.equals(getIntent().getAction())) {
312 /// enabling pass code
313 if (!mConfirmingPassCode
) {
314 requestPassCodeConfirmation();
316 } else if (confirmPassCode()) {
317 /// confirmed: user typed the same pass code twice
318 savePassCodeAndExit();
322 R
.string
.pass_code_mismatch
, R
.string
.pass_code_configure_your_pass_code
, View
.VISIBLE
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
341 * Ask to the user for retyping the pass code just entered before saving it as the current pass
344 protected void requestPassCodeConfirmation(){
346 mPassCodeHdr
.setText(R
.string
.pass_code_reenter_your_pass_code
);
347 mPassCodeHdrExplanation
.setVisibility(View
.INVISIBLE
);
348 mConfirmingPassCode
= true
;
352 * Compares pass code entered by the user with the value currently saved in the app.
354 * @return 'True' if entered pass code equals to the saved one.
356 protected boolean checkPassCode(){
357 SharedPreferences appPrefs
= PreferenceManager
358 .getDefaultSharedPreferences(getApplicationContext());
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
);
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
]);
375 * Compares pass code retyped by the user in the input fields with the value entered just
378 * @return 'True' if retyped pass code equals to the entered before.
380 protected boolean confirmPassCode(){
381 mConfirmingPassCode
= false
;
383 boolean result
= true
;
384 for (int i
= 0; i
< mPassCodeEditTexts
.length
&& result
; i
++) {
386 ((mPassCodeEditTexts
[i
].getText().toString()).equals(mPassCodeDigits
[i
]));
392 * Sets the input fields to empty strings and puts the focus on the first one.
394 protected void clearBoxes(){
395 for (int i
=0; i
< mPassCodeEditTexts
.length
; i
++) {
396 mPassCodeEditTexts
[i
].setText("");
398 mPassCodeEditTexts
[0].requestFocus();
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.
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.
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();
418 return super.onKeyDown(keyCode
, event
);
422 * Saves the pass code input by the user as the current pass code.
424 protected void savePassCodeAndExit() {
425 SharedPreferences
.Editor appPrefs
= PreferenceManager
426 .getDefaultSharedPreferences(getApplicationContext()).edit();
428 Intent resultIntent
= new Intent();
429 resultIntent
.putExtra(KEY_PASSCODE
,
430 mPassCodeDigits
[0] + mPassCodeDigits
[1] + mPassCodeDigits
[2] + mPassCodeDigits
[3]);
432 setResult(RESULT_OK
, resultIntent
);
437 * Cancellation of ACTION_ENABLE or ACTION_DISABLE; reverts the enable or disable action done by
438 * {@link Preferences}, then finishes.
440 protected void revertActionAndExit() {
441 SharedPreferences
.Editor appPrefsE
= PreferenceManager
442 .getDefaultSharedPreferences(getApplicationContext()).edit();
444 SharedPreferences appPrefs
= PreferenceManager
445 .getDefaultSharedPreferences(getApplicationContext());
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
451 // TODO in this activity; was the PreferenceCheckBox in the caller who did it
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
);
463 private class PassCodeDigitTextWatcher
implements TextWatcher
{
465 private int mIndex
= -1;
466 private boolean mLastOne
= false
;
471 * @param index Position in the pass code of the input field that will be bound to
473 * @param lastOne 'True' means that watcher corresponds to the last position of the
476 public PassCodeDigitTextWatcher(int index
, boolean lastOne
) {
480 throw new IllegalArgumentException(
481 "Invalid index in " + PassCodeDigitTextWatcher
.class.getSimpleName() +
488 return mLastOne ?
0 : mIndex
+ 1;
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
501 public void afterTextChanged(Editable s
) {
502 if (s
.length() > 0) {
503 if (!mConfirmingPassCode
) {
504 mPassCodeDigits
[mIndex
] = mPassCodeEditTexts
[mIndex
].getText().toString();
506 mPassCodeEditTexts
[next()].requestFocus();
509 processFullPassCode();
513 Log_OC
.d(TAG
, "Text box " + mIndex
+ " was cleaned");
518 public void beforeTextChanged(CharSequence s
, int start
, int count
, int after
) {
523 public void onTextChanged(CharSequence s
, int start
, int before
, int count
) {