PassCodeActivity.java 19 KB

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