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
.SharedPreferences
;
28 import android
.os
.Bundle
;
29 import android
.preference
.PreferenceManager
;
30 import android
.text
.Editable
;
31 import android
.text
.TextWatcher
;
32 import android
.view
.KeyEvent
;
33 import android
.view
.View
;
34 import android
.view
.View
.OnClickListener
;
35 import android
.widget
.Button
;
36 import android
.widget
.EditText
;
37 import android
.widget
.TextView
;
38 import android
.widget
.Toast
;
40 import com
.actionbarsherlock
.app
.ActionBar
;
41 import com
.actionbarsherlock
.app
.SherlockFragmentActivity
;
42 import com
.owncloud
.android
.R
;
43 import com
.owncloud
.android
.lib
.common
.utils
.Log_OC
;
44 import com
.owncloud
.android
.utils
.DisplayUtils
;
46 public class PassCodeActivity
extends SherlockFragmentActivity
{
49 private static final String TAG
= PassCodeActivity
.class.getSimpleName();
51 public final static String ACTION_ENABLE
= PassCodeActivity
.class.getCanonicalName() + ".ENABLE";
52 public final static String ACTION_DISABLE
= PassCodeActivity
.class.getCanonicalName() + ".DISABLE";
53 public final static String ACTION_REQUEST
= PassCodeActivity
.class.getCanonicalName() + ".REQUEST";
55 private Button mBCancel
;
56 private TextView mPassCodeHdr
;
57 private TextView mPassCodeHdrExplanation
;
58 private EditText
[] mPassCodeEditTexts
= new EditText
[4];
60 private String
[] mPassCodeDigits
= {"","","",""};
61 private boolean mConfirmingPassCode
= false
;
63 private boolean mBChange
= true
; // to control that only one blocks jump
67 * Initializes the activity.
69 * An intent with a valid ACTION is expected; if none is found, an {@link IllegalArgumentException} will be thrown.
71 * @param savedInstanceState Previously saved state - irrelevant in this case
73 protected void onCreate(Bundle savedInstanceState
) {
74 super.onCreate(savedInstanceState
);
75 setContentView(R
.layout
.passcodelock
);
77 mBCancel
= (Button
) findViewById(R
.id
.cancel
);
78 mPassCodeHdr
= (TextView
) findViewById(R
.id
.header
);
79 mPassCodeHdrExplanation
= (TextView
) findViewById(R
.id
.explanation
);
80 mPassCodeEditTexts
[0] = (EditText
) findViewById(R
.id
.txt0
);
81 mPassCodeEditTexts
[0].requestFocus();
82 getWindow().setSoftInputMode(android
.view
.WindowManager
.LayoutParams
.SOFT_INPUT_STATE_VISIBLE
);
83 mPassCodeEditTexts
[1] = (EditText
) findViewById(R
.id
.txt1
);
84 mPassCodeEditTexts
[2] = (EditText
) findViewById(R
.id
.txt2
);
85 mPassCodeEditTexts
[3] = (EditText
) findViewById(R
.id
.txt3
);
87 if (ACTION_REQUEST
.equals(getIntent().getAction())) {
88 /// this is a pass code request; the user has to input the right value
89 mPassCodeHdr
.setText(R
.string
.pass_code_enter_pass_code
);
90 mPassCodeHdrExplanation
.setVisibility(View
.INVISIBLE
);
91 setCancelButtonEnabled(false
); // no option to cancel
93 } else if (ACTION_ENABLE
.equals(getIntent().getAction())) {
94 /// pass code preference has just been activated in Preferences; will receive and confirm pass code value
95 mPassCodeHdr
.setText(R
.string
.pass_code_configure_your_pass_code
);
96 //mPassCodeHdr.setText(R.string.pass_code_enter_pass_code); // TODO choose a header, check iOS
97 mPassCodeHdrExplanation
.setVisibility(View
.VISIBLE
);
98 setCancelButtonEnabled(true
);
100 } else if (ACTION_DISABLE
.equals(getIntent().getAction())) {
101 /// pass code preference has just been disabled in Preferences;
102 // will confirm user knows pass code, then remove it
103 mPassCodeHdr
.setText(R
.string
.pass_code_remove_your_pass_code
);
104 mPassCodeHdrExplanation
.setVisibility(View
.INVISIBLE
);
105 setCancelButtonEnabled(true
);
108 throw new IllegalArgumentException("A valid ACTION is needed in the Intent passed to " + TAG
);
113 ActionBar actionBar
= getSupportActionBar();
114 actionBar
.setIcon(DisplayUtils
.getSeasonalIconId());
119 * Enables or disables the cancel button to allow the user interrupt the ACTION requested to the activity.
121 * @param enabled 'True' makes the cancel button available, 'false' hides it.
123 protected void setCancelButtonEnabled(boolean enabled
){
125 mBCancel
.setVisibility(View
.VISIBLE
);
126 mBCancel
.setOnClickListener(new OnClickListener() {
128 public void onClick(View v
) {
129 revertActionAndExit();
133 mBCancel
.setVisibility(View
.GONE
);
134 mBCancel
.setVisibility(View
.INVISIBLE
);
135 mBCancel
.setOnClickListener(null
);
141 * Binds the appropiate listeners to the input boxes receiving each digit of the pass code.
143 protected void setTextListeners() {
145 /// First input field
146 mPassCodeEditTexts
[0].addTextChangedListener(new PassCodeDigitTextWatcher(0, false
));
149 /*------------------------------------------------
151 -------------------------------------------------*/
152 mPassCodeEditTexts
[1].addTextChangedListener(new PassCodeDigitTextWatcher(1, false
));
154 mPassCodeEditTexts
[1].setOnKeyListener(new View
.OnKeyListener() {
157 public boolean onKey(View v
, int keyCode
, KeyEvent event
) {
158 if (keyCode
== KeyEvent
.KEYCODE_DEL
&& mBChange
) { // TODO WIP: event should be used to control what's exactly happening with DEL, not any custom field...
159 mPassCodeEditTexts
[0].setText("");
160 mPassCodeEditTexts
[0].requestFocus();
161 if (!mConfirmingPassCode
)
162 mPassCodeDigits
[0] = "";
165 } else if (!mBChange
) {
172 mPassCodeEditTexts
[1].setOnFocusChangeListener(new View
.OnFocusChangeListener() {
175 public void onFocusChange(View v
, boolean hasFocus
) {
176 /// TODO WIP: should take advantage of hasFocus to reduce processing
177 mPassCodeEditTexts
[1].setCursorVisible(true
); // TODO WIP this could be made static, or just nothing, since default is true
178 if (mPassCodeEditTexts
[0].getText().toString().equals("")) { // TODO WIP validation could be done in a global way, with a single OnFocusChangeListener for all the input fields
179 mPassCodeEditTexts
[1].setSelected(false
);
180 mPassCodeEditTexts
[1].setCursorVisible(false
); // TODO WIP don't think we really need to enable and disable it; fields without focus should not show it
181 mPassCodeEditTexts
[0].requestFocus();
182 mPassCodeEditTexts
[0].setSelected(true
); // TODO WIP really needed? is it for the colour highlight
183 mPassCodeEditTexts
[0].setSelection(0); // TODO WIP what is THIS for?
190 /*------------------------------------------------
192 -------------------------------------------------*/
193 mPassCodeEditTexts
[2].addTextChangedListener(new PassCodeDigitTextWatcher(2, false
));
195 mPassCodeEditTexts
[2].setOnKeyListener(new View
.OnKeyListener() {
198 public boolean onKey(View v
, int keyCode
, KeyEvent event
) {
199 if (keyCode
== KeyEvent
.KEYCODE_DEL
&& mBChange
) {
200 mPassCodeEditTexts
[1].requestFocus();
201 if (!mConfirmingPassCode
)
202 mPassCodeDigits
[1] = "";
203 mPassCodeEditTexts
[1].setText("");
206 } else if (!mBChange
) {
214 mPassCodeEditTexts
[2].setOnFocusChangeListener(new View
.OnFocusChangeListener() {
217 public void onFocusChange(View v
, boolean hasFocus
) {
218 mPassCodeEditTexts
[2].setCursorVisible(true
);
219 if (mPassCodeEditTexts
[0].getText().toString().equals("")) {
220 mPassCodeEditTexts
[2].setSelected(false
);
221 mPassCodeEditTexts
[2].setCursorVisible(false
);
222 mPassCodeEditTexts
[0].requestFocus();
223 mPassCodeEditTexts
[0].setSelected(true
);
224 mPassCodeEditTexts
[0].setSelection(0);
225 } else if (mPassCodeEditTexts
[1].getText().toString().equals("")) {
226 mPassCodeEditTexts
[2].setSelected(false
);
227 mPassCodeEditTexts
[2].setCursorVisible(false
);
228 mPassCodeEditTexts
[1].requestFocus();
229 mPassCodeEditTexts
[1].setSelected(true
);
230 mPassCodeEditTexts
[1].setSelection(0);
237 /*------------------------------------------------
239 -------------------------------------------------*/
240 mPassCodeEditTexts
[3].addTextChangedListener(new PassCodeDigitTextWatcher(3, true
));
242 mPassCodeEditTexts
[3].setOnKeyListener(new View
.OnKeyListener() {
245 public boolean onKey(View v
, int keyCode
, KeyEvent event
) {
246 if (keyCode
== KeyEvent
.KEYCODE_DEL
&& mBChange
) {
247 mPassCodeEditTexts
[2].requestFocus();
248 if (!mConfirmingPassCode
)
249 mPassCodeDigits
[2] = "";
250 mPassCodeEditTexts
[2].setText("");
253 } else if (!mBChange
) {
260 mPassCodeEditTexts
[3].setOnFocusChangeListener(new View
.OnFocusChangeListener() {
263 public void onFocusChange(View v
, boolean hasFocus
) {
264 mPassCodeEditTexts
[3]. setCursorVisible(true
);
266 if (mPassCodeEditTexts
[0].getText().toString().equals("")) {
267 mPassCodeEditTexts
[3].setSelected(false
);
268 mPassCodeEditTexts
[3].setCursorVisible(false
);
269 mPassCodeEditTexts
[0].requestFocus();
270 mPassCodeEditTexts
[0].setSelected(true
);
271 mPassCodeEditTexts
[0].setSelection(0);
272 } else if (mPassCodeEditTexts
[1].getText().toString().equals("")) {
273 mPassCodeEditTexts
[3].setSelected(false
);
274 mPassCodeEditTexts
[3].setCursorVisible(false
);
275 mPassCodeEditTexts
[1].requestFocus();
276 mPassCodeEditTexts
[1].setSelected(true
);
277 mPassCodeEditTexts
[1].setSelection(0);
278 } else if (mPassCodeEditTexts
[2].getText().toString().equals("")) {
279 mPassCodeEditTexts
[3].setSelected(false
);
280 mPassCodeEditTexts
[3].setCursorVisible(false
);
281 mPassCodeEditTexts
[2].requestFocus();
282 mPassCodeEditTexts
[2].setSelected(true
);
283 mPassCodeEditTexts
[2].setSelection(0);
289 } // end setTextListener
293 * Processes the pass code entered by the user just after the last digit was in.
295 * Takes into account the action requested to the activity, the currently saved pass code and the previously
296 * typed pass code, if any.
298 private void processFullPassCode() {
299 if (ACTION_REQUEST
.equals(getIntent().getAction())) {
300 if (checkPassCode()) {
301 /// pass code accepted in request, user is allowed to access the app
305 showErrorAndRestart(R
.string
.common_error
, R
.string
.pass_code_enter_pass_code
, View
.INVISIBLE
);
306 /// TODO better error message
309 } else if (ACTION_DISABLE
.equals(getIntent().getAction())) {
310 if (checkPassCode()) {
311 /// pass code accepted when disabling, pass code is removed
312 SharedPreferences
.Editor appPrefs
= PreferenceManager
313 .getDefaultSharedPreferences(getApplicationContext()).edit();
314 appPrefs
.putBoolean("set_pincode", false
); // TODO remove; this should be unnecessary, was done before entering in the activity
317 Toast
.makeText(PassCodeActivity
.this, R
.string
.pass_code_removed
, Toast
.LENGTH_LONG
).show();
321 showErrorAndRestart(R
.string
.common_error
, R
.string
.pass_code_enter_pass_code
, View
.INVISIBLE
);
322 /// TODO better error message
325 } else if (ACTION_ENABLE
.equals(getIntent().getAction())) {
326 /// enabling pass code
327 if (!mConfirmingPassCode
) {
328 requestPassCodeConfirmation();
330 } else if (confirmPassCode()) {
331 /// confirmed: user typed the same pass code twice
332 savePassCodeAndExit();
336 R
.string
.pass_code_mismatch
, R
.string
.pass_code_configure_your_pass_code
, View
.VISIBLE
343 private void showErrorAndRestart(int errorMessage
, int headerMessage
, int explanationVisibility
) {
344 Arrays
.fill(mPassCodeDigits
, null
);
345 CharSequence errorSeq
= getString(errorMessage
);
346 Toast
.makeText(this, errorSeq
, Toast
.LENGTH_LONG
).show();
347 mPassCodeHdr
.setText(headerMessage
); // TODO check if really needed
348 mPassCodeHdrExplanation
.setVisibility(explanationVisibility
); // TODO check if really needed
354 * Ask to the user for retyping the pass code just entered before saving it as the current pass code.
356 protected void requestPassCodeConfirmation(){
358 mPassCodeHdr
.setText(R
.string
.pass_code_reenter_your_pass_code
);
359 mPassCodeHdrExplanation
.setVisibility(View
.INVISIBLE
);
360 mConfirmingPassCode
= true
;
364 * Compares pass code entered by the user with the value currently saved in the app.
366 * @return 'True' if entered pass code equals to the saved one.
368 protected boolean checkPassCode(){
369 SharedPreferences appPrefs
= PreferenceManager
370 .getDefaultSharedPreferences(getApplicationContext());
372 String savedPassCodeDigits
[] = new String
[4];
373 savedPassCodeDigits
[0] = appPrefs
.getString("PrefPinCode1", null
);
374 savedPassCodeDigits
[1] = appPrefs
.getString("PrefPinCode2", null
);
375 savedPassCodeDigits
[2] = appPrefs
.getString("PrefPinCode3", null
);
376 savedPassCodeDigits
[3] = appPrefs
.getString("PrefPinCode4", null
);
378 boolean result
= true
;
379 for (int i
= 0; i
< mPassCodeDigits
.length
&& result
; i
++) {
380 result
= result
&& (mPassCodeDigits
[i
] != null
) && mPassCodeDigits
[i
].equals(savedPassCodeDigits
[i
]);
386 * Compares pass code retyped by the user in the input fields with the value entered just before.
388 * @return 'True' if retyped pass code equals to the entered before.
390 protected boolean confirmPassCode(){
391 mConfirmingPassCode
= false
;
393 boolean result
= true
;
394 for (int i
= 0; i
< mPassCodeEditTexts
.length
&& result
; i
++) {
395 result
= result
&& ((mPassCodeEditTexts
[i
].getText().toString()).equals(mPassCodeDigits
[i
]));
401 * Sets the input fields to empty strings and puts the focus on the first one.
403 protected void clearBoxes(){
404 for (int i
=0; i
< mPassCodeEditTexts
.length
; i
++) {
405 mPassCodeEditTexts
[i
].setText("");
407 mPassCodeEditTexts
[0].requestFocus();
411 * Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while preventing
412 * than ACTION_REQUEST may be worked around.
414 * @param keyCode Key code of the key that triggered the down event.
415 * @param event Event triggered.
416 * @return 'True' when the key event was processed by this method.
419 public boolean onKeyDown(int keyCode
, KeyEvent event
){
420 if (keyCode
== KeyEvent
.KEYCODE_BACK
&& event
.getRepeatCount()== 0){
421 if (ACTION_ENABLE
.equals(getIntent().getAction()) || ACTION_DISABLE
.equals(getIntent().getAction())) {
422 revertActionAndExit();
426 return super.onKeyDown(keyCode
, event
);
430 * Saves the pass code input by the user as the current pass code.
432 protected void savePassCodeAndExit() {
433 SharedPreferences
.Editor appPrefs
= PreferenceManager
434 .getDefaultSharedPreferences(getApplicationContext()).edit();
436 appPrefs
.putString("PrefPinCode1", mPassCodeDigits
[0]);
437 appPrefs
.putString("PrefPinCode2", mPassCodeDigits
[1]);
438 appPrefs
.putString("PrefPinCode3", mPassCodeDigits
[2]);
439 appPrefs
.putString("PrefPinCode4", mPassCodeDigits
[3]);
440 appPrefs
.putBoolean("set_pincode", true
); /// TODO remove; unnecessary, Preferences did it before entering here
443 Toast
.makeText(this, R
.string
.pass_code_stored
, Toast
.LENGTH_LONG
).show();
448 * Cancellation of ACTION_ENABLE or ACTION_DISABLE; reverts the enable or disable action done by
449 * {@link Preferences}, then finishes.
451 protected void revertActionAndExit() {
452 SharedPreferences
.Editor appPrefsE
= PreferenceManager
453 .getDefaultSharedPreferences(getApplicationContext()).edit();
455 SharedPreferences appPrefs
= PreferenceManager
456 .getDefaultSharedPreferences(getApplicationContext());
458 boolean state
= appPrefs
.getBoolean("set_pincode", false
);
459 appPrefsE
.putBoolean("set_pincode", !state
);
460 // TODO WIP: this is reverting the value of the preference because it was changed BEFORE entering
461 // TODO in this activity; was the PreferenceCheckBox in the caller who did it
467 private class PassCodeDigitTextWatcher
implements TextWatcher
{
469 private int mIndex
= -1;
470 private boolean mLastOne
= false
;
475 * @param index Position in the pass code of the input field that will be bound to this watcher.
476 * @param lastOne 'True' means that watcher corresponds to the last position of the pass code.
478 public PassCodeDigitTextWatcher(int index
, boolean lastOne
) {
482 throw new IllegalArgumentException(
483 "Invalid index in " + PassCodeDigitTextWatcher
.class.getSimpleName() + " constructor"
489 return mLastOne ?
0 : mIndex
+ 1;
493 * Performs several actions when the user types a digit in an input field:
494 * - saves the input digit to the state of the activity; this will allow retyping the 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
) {