서비스 중인 체크리스트 앱에는 비밀번호 잠그기 기능이 있습니다. 이것을 어떻게 구현했는지 알아보겠습니다.
잠그기 기능은 android_base 서브 모듈에 구현했습니다. 잠그기 기능이 여러 앱에서 사용될 수 있기 때문에 공통 모듈에 구현하는 것이 좋겠다고 생각했습니다.
CommonSettings : 비밀번호와 잠그기 기능을 사용할지 여부를 설정변수로 선언합니다.
// passcode public static final SettingString userPasscode = new SettingString("user_passcode", ""); public static final SettingBoolean enabledPasscode = new SettingBoolean("enabled_passcode", false);
PassCodeManager : 매우 간단히 구현했습니다. 보통 다른 앱의 잠그기 기능은 앱에 진입할 때마다 비밀번호를 확인합니다. 방금 전에 확인했는데도 잠깐 다른 앱을 쓴다든지 외부 Activity를 호출한 후에 다시 돌아오면 다시 비밀번호를 확인해야 해서 너무 자주 확인하는 것이 많이 불편하다고 생각했습니다. 제 구현은 비밀번호의 확인 후 앱을 사용하지 않은 상태에서 10분 이상 지나면 다시 비밀번호를 확인합니다. 10분 이내에 핸드폰을 분실해서 무슨 문제가 생기지는 않겠죠. onResume, onStop, onBackPressed는 BaseActivity에서 호출합니다. 즉 앱의 모든 Activity에서 호출됩니다.
public class PassCodeManager { public static final String TAG = "PassCodeManager"; long mTimeStamp = Long.MAX_VALUE; private static final int LOCK_TIME = 10 * 60 * 1000; // 10min. private static class LazyHolder { private static final PassCodeManager INSTANCE = new PassCodeManager(); } public static PassCodeManager getInstance() { return LazyHolder.INSTANCE; } private PassCodeManager() { if (CommonSettings.enabledPasscode.getValue()) { mTimeStamp = 0; } } public void onResume(Context context) { if (CommonSettings.enabledPasscode.getValue()) { if ((System.currentTimeMillis() - mTimeStamp) >= LOCK_TIME) { if (!(context instanceof PassCodeActivity)) { PassCodeActivity.startActivity(context, PassCodeMode.Confirm); } } } mTimeStamp = Long.MAX_VALUE; } public void onStop(Context context) { mTimeStamp = System.currentTimeMillis(); } public void onBackPressed(Context context) { mTimeStamp = System.currentTimeMillis(); } public boolean enabledPasscode() { return CommonSettings.enabledPasscode.getValue(); } public void setPassCode(String newPasscode) { CommonSettings.enabledPasscode.setValue(StringUtils.isNotEmpty(newPasscode)); CommonSettings.userPasscode.setValue(newPasscode); } public boolean isValidPasscode(String passcode) { return passcode.equals(CommonSettings.userPasscode.getValue()); } }
BaseActivity에 적용하기
public class BaseActivity extends AppCompatActivity { @Override protected void onResume() { super.onResume(); PassCodeManager.getInstance().onResume(this); } @Override protected void onStop() { super.onStop(); PassCodeManager.getInstance().onStop(this); } @Override public void onBackPressed() { PassCodeManager.getInstance().onBackPressed(this); super.onBackPressed(); } }
PassCodeMode : PassCodeActivity를 호출하는 모드는 3가지가 있습니다. 잠그기, 잠그기 해제하기, 비밀번호 확인하기
public enum PassCodeMode { Lock, Unlock, Confirm }
PassCodeActivity: 역시 최소한으로 구현했습니다. 초기에 작성된 코드라서 코딩 컨벤션이 맞지 않네요.
public class PassCodeActivity extends BaseActivity { Handler mHandler = new Handler(); PassCodeMode mMode = PassCodeMode.Confirm; int[] mPasscode1 = new int[4]; int[] mPasscode2 = new int[4]; ImageView[] mImages = new ImageView[4]; int mInputCount = 0; boolean mSecondPhase = false; TextView titleView; TextView subTitleView; static final int mEmptyBitmap = R.drawable.ic_checkbox_blank_circle_outline_grey600_48dp; static final int mOkBitmap = R.drawable.ic_check_circle_outline_grey600_48dp; public static void startActivity(Context from, PassCodeMode mode) { Intent intent = new Intent(from, PassCodeActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra("mode", mode.ordinal()); from.startActivity(intent); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(R.layout.passcode_activity); int mode = getIntent().getIntExtra("mode", PassCodeMode.Confirm.ordinal()); for (PassCodeMode m : PassCodeMode.values()) { if (m.ordinal() == mode) { mMode = m; break; } } if (mMode == PassCodeMode.Lock || mMode == PassCodeMode.Unlock) { if (this.getSupportActionBar() != null) { this.getSupportActionBar().setDisplayHomeAsUpEnabled(true); } } titleView = (TextView) findViewById(R.id.title); subTitleView = (TextView) findViewById(R.id.subTitle); mImages[0] = (ImageView) findViewById(R.id.imageView01); mImages[1] = (ImageView) findViewById(R.id.imageView02); mImages[2] = (ImageView) findViewById(R.id.imageView03); mImages[3] = (ImageView) findViewById(R.id.imageView04); findViewById(R.id.button_00).setOnClickListener(mOnClickListener); findViewById(R.id.button_01).setOnClickListener(mOnClickListener); findViewById(R.id.button_02).setOnClickListener(mOnClickListener); findViewById(R.id.button_03).setOnClickListener(mOnClickListener); findViewById(R.id.button_04).setOnClickListener(mOnClickListener); findViewById(R.id.button_05).setOnClickListener(mOnClickListener); findViewById(R.id.button_06).setOnClickListener(mOnClickListener); findViewById(R.id.button_07).setOnClickListener(mOnClickListener); findViewById(R.id.button_08).setOnClickListener(mOnClickListener); findViewById(R.id.button_09).setOnClickListener(mOnClickListener); findViewById(R.id.buttonCancel).setOnClickListener(mOnClickListener); findViewById(R.id.buttonBackspace).setOnClickListener(mOnClickListener); if (mMode == PassCodeMode.Lock) { titleView.setText(R.string.passcode_set_passcode); subTitleView.setText(R.string.passcode_enter_a_passcode); } else if (mMode == PassCodeMode.Confirm) { titleView.setText(R.string.passcode_confirm_passcode); subTitleView.setText(R.string.passcode_enter_your_passcode); } else if (mMode == PassCodeMode.Unlock) { titleView.setText(R.string.passcode_unlock_passcode); subTitleView.setText(R.string.passcode_enter_your_passcode); } } @Override public void onBackPressed() { if (mMode == PassCodeMode.Lock || mMode == PassCodeMode.Unlock) { finish(); } else if (mMode == PassCodeMode.Confirm) { //System.exit(0); moveTaskToBack(true); } } private void onButtonCancel() { if (mMode == PassCodeMode.Lock || mMode == PassCodeMode.Unlock) { finish(); } else if (mMode == PassCodeMode.Confirm) { //System.exit(0); moveTaskToBack(true); } } private int[] ids = new int[]{ R.id.button_00 , R.id.button_01 , R.id.button_02 , R.id.button_03 , R.id.button_04 , R.id.button_05 , R.id.button_06 , R.id.button_07 , R.id.button_08 , R.id.button_09 }; private void onButtonNumbers(int id) { if (mInputCount >= 4) { return; } int number = 0; for (number = 0; number <= ids.length; number++) { if (id == ids[number]) { break; } } if (mSecondPhase) { mPasscode2[mInputCount] = number; } else { mPasscode1[mInputCount] = number; } mImages[mInputCount].setImageResource(mOkBitmap); mInputCount++; if (mMode == PassCodeMode.Lock && mInputCount == 4 && mSecondPhase) { boolean isSame = true; String passcode = ""; for (int i = 0; i < 4; i++) { if (mPasscode1[i] != mPasscode2[i]) { isSame = false; break; } passcode += String.valueOf(mPasscode1[i]); } if (isSame) { if (BuildConfig.DEBUG) { DebugUtils.toast("new passcode is " + passcode); } PassCodeManager.getInstance().setPassCode(passcode); finish(); } else { mHandler.postDelayed(new Runnable() { @Override public void run() { subTitleView.setText(R.string.passcode_not_matched); mInputCount = 0; mSecondPhase = false; for (ImageView image : mImages) { image.setImageResource(mEmptyBitmap); } } }, 200); } } else if (mInputCount == 4) { if (mMode == PassCodeMode.Lock) { subTitleView.setText(R.string.passcode_enter_your_passcode); mInputCount = 0; mHandler.postDelayed(new Runnable() { @Override public void run() { mSecondPhase = true; for (ImageView image : mImages) { image.setImageResource(mEmptyBitmap); } } }, 200); } else { //unlock, confirm String passcode = ""; for (int i = 0; i < 4; i++) { passcode += String.valueOf(mPasscode1[i]); } final String paramPasscode = passcode; mHandler.postDelayed(new Runnable() { @Override public void run() { if (PassCodeManager.getInstance().isValidPasscode(paramPasscode)) { if (mMode == PassCodeMode.Unlock) { PassCodeManager.getInstance().setPassCode(""); } finish(); } else { subTitleView.setText(R.string.passcode_invalid_passcode); mInputCount = 0; for (ImageView image : mImages) { image.setImageResource(mEmptyBitmap); } } } }, 200); } } } private void onButtonBackspace() { if (mInputCount > 0) { mInputCount--; } if (mInputCount >= 0) { mImages[mInputCount].setImageResource(mEmptyBitmap); } } private View.OnClickListener mOnClickListener = new View.OnClickListener() { @Override public void onClick(View v) { if (v.getId() == R.id.buttonCancel) { onButtonCancel(); } else if (v.getId() == R.id.buttonBackspace) { onButtonBackspace(); } else { onButtonNumbers(v.getId()); } } };