Commit fc86319e authored by Torsten Grote's avatar Torsten Grote

MVP of PIN lock feature

This only shows the settings if a screen lock is available.
If the setting is activated, it shows a drawer item for locking.
Once locked, the screen lock secret needs to be entered to enter the
app again.

First part of #1247
parent af1ed292
......@@ -67,4 +67,8 @@ public interface AccountManager {
* and stored.
*/
boolean changePassword(String oldPassword, String newPassword);
boolean isLocked();
void setLocked(boolean locked);
}
......@@ -45,6 +45,7 @@ class AccountManagerImpl implements AccountManager {
@Nullable
private volatile SecretKey databaseKey = null;
private volatile boolean locked = false;
@Inject
AccountManagerImpl(DatabaseConfig databaseConfig, CryptoComponent crypto,
......@@ -218,4 +219,16 @@ class AccountManagerImpl implements AccountManager {
return key != null && encryptAndStoreDatabaseKey(key, newPassword);
}
}
@Override
public boolean isLocked() {
return locked;
}
@Override
public void setLocked(boolean locked) {
synchronized (stateChangeLock) {
this.locked = locked;
}
}
}
......@@ -402,5 +402,11 @@
android:theme="@android:style/Theme.NoDisplay">
</activity>
<activity
android:name=".android.login.UnlockActivity"
android:label="@string/lock_unlock"
android:launchMode="singleTask"
android:theme="@style/BriarTheme.NoActionBar"/>
</application>
</manifest>
......@@ -40,4 +40,9 @@ public interface TestingConstants {
* Feature flag for enabling the sign-in reminder in release builds.
*/
boolean FEATURE_FLAG_SIGN_IN_REMINDER = IS_DEBUG_BUILD;
/**
* Feature flag for enabling the PIN lock in release builds.
*/
boolean FEATURE_FLAG_PIN_LOCK = IS_DEBUG_BUILD;
}
......@@ -37,6 +37,7 @@ import org.briarproject.briar.android.login.OpenDatabaseActivity;
import org.briarproject.briar.android.login.PasswordActivity;
import org.briarproject.briar.android.login.PasswordFragment;
import org.briarproject.briar.android.login.SetupActivity;
import org.briarproject.briar.android.login.UnlockActivity;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.panic.PanicPreferencesActivity;
import org.briarproject.briar.android.panic.PanicResponderActivity;
......@@ -163,6 +164,8 @@ public interface ActivityComponent {
void inject(StartupFailureActivity activity);
void inject(UnlockActivity activity);
// Fragments
void inject(AuthorNameFragment fragment);
......
......@@ -16,6 +16,7 @@ import org.briarproject.briar.android.controller.BriarController;
import org.briarproject.briar.android.controller.DbController;
import org.briarproject.briar.android.controller.handler.UiResultHandler;
import org.briarproject.briar.android.login.PasswordActivity;
import org.briarproject.briar.android.login.UnlockActivity;
import org.briarproject.briar.android.logout.ExitActivity;
import java.util.logging.Logger;
......@@ -30,6 +31,7 @@ import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
import static android.os.Build.VERSION.SDK_INT;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_DOZE_WHITELISTING;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_UNLOCK;
import static org.briarproject.briar.android.util.UiUtils.getDozeWhitelistingIntent;
import static org.briarproject.briar.android.util.UiUtils.isSamsung7;
......@@ -55,6 +57,8 @@ public abstract class BriarActivity extends BaseActivity {
if (request == REQUEST_PASSWORD) {
if (result == RESULT_OK) briarController.startAndBindService();
else supportFinishAfterTransition();
} else if (request == REQUEST_UNLOCK) {
if (result != RESULT_OK) supportFinishAfterTransition();
}
}
......@@ -64,6 +68,9 @@ public abstract class BriarActivity extends BaseActivity {
if (!briarController.accountSignedIn() && !isFinishing()) {
Intent i = new Intent(this, PasswordActivity.class);
startActivityForResult(i, REQUEST_PASSWORD);
} else if(briarController.isLocked()) {
Intent i = new Intent(this, UnlockActivity.class);
startActivityForResult(i, REQUEST_UNLOCK);
} else if (SDK_INT >= 23) {
briarController.hasDozed(new UiResultHandler<Boolean>(this) {
@Override
......
......@@ -12,5 +12,6 @@ public interface RequestCodes {
int REQUEST_PERMISSION_CAMERA = 8;
int REQUEST_DOZE_WHITELISTING = 9;
int REQUEST_ENABLE_BLUETOOTH = 10;
int REQUEST_UNLOCK = 11;
}
......@@ -8,6 +8,8 @@ public interface BriarController extends ActivityLifecycleController {
boolean accountSignedIn();
boolean isLocked();
/**
* Returns true via the handler when the app has dozed
* without being white-listed.
......
......@@ -87,6 +87,11 @@ public class BriarControllerImpl implements BriarController {
return accountManager.hasDatabaseKey();
}
@Override
public boolean isLocked() {
return accountManager.isLocked();
}
@Override
public void hasDozed(ResultHandler<Boolean> handler) {
if (!dozeWatchdog.getAndResetDozeFlag()
......
package org.briarproject.briar.android.login;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v4.app.ActivityCompat;
import android.widget.Button;
import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BaseActivity;
import java.util.logging.Logger;
import javax.inject.Inject;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_UNLOCK;
@RequiresApi(21)
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class UnlockActivity extends BaseActivity {
private static final Logger LOG =
Logger.getLogger(UnlockActivity.class.getSimpleName());
@Inject
AccountManager accountManager;
@Override
public void injectActivity(ActivityComponent component) {
component.inject(this);
}
public void onCreate(@Nullable Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_unlock);
Button button = findViewById(R.id.unlock);
button.setOnClickListener(view -> requestKeyguardUnlock());
requestKeyguardUnlock();
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_UNLOCK && resultCode == RESULT_OK) {
unlock();
}
}
private void requestKeyguardUnlock() {
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(
Context.KEYGUARD_SERVICE);
assert keyguardManager != null;
Intent intent = keyguardManager
.createConfirmDeviceCredentialIntent(getString(R.string.lock_unlock),
null);
if (intent == null) {
// the user must have removed the screen lock since locked
LOG.warning("Unlocking without keyguard");
unlock();
} else {
startActivityForResult(intent, REQUEST_UNLOCK);
}
}
private void unlock() {
accountManager.setLocked(false);
setResult(RESULT_OK);
ActivityCompat.finishAfterTransition(this);
}
}
......@@ -7,6 +7,7 @@ import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.NavigationView;
import android.support.design.widget.NavigationView.OnNavigationItemSelectedListener;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.ContextCompat;
......@@ -59,6 +60,7 @@ import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSW
import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.NO;
import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.UPDATE;
import static org.briarproject.briar.android.util.UiUtils.getDaysUntilExpiry;
import static org.briarproject.briar.android.util.UiUtils.hasScreenLock;
public class NavDrawerActivity extends BriarActivity implements
BaseFragmentListener, TransportStateListener,
......@@ -152,6 +154,14 @@ public class NavDrawerActivity extends BriarActivity implements
public void onStart() {
super.onStart();
updateTransports();
if (hasScreenLock(this)) {
controller.isLockable(new UiResultHandler<Boolean>(this) {
@Override
public void onResultUi(@NonNull Boolean lockable) {
setLockVisible(lockable);
}
});
} else setLockVisible(false);
controller.showExpiryWarning(new UiResultHandler<ExpiryWarning>(this) {
@Override
public void onResultUi(ExpiryWarning expiry) {
......@@ -213,9 +223,15 @@ public class NavDrawerActivity extends BriarActivity implements
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
drawerLayout.closeDrawer(START);
clearBackStack();
loadFragment(item.getItemId());
// Don't display the Settings item as checked
return item.getItemId() != R.id.nav_btn_settings;
if (item.getItemId() == R.id.nav_btn_lock) {
controller.lock();
ActivityCompat.finishAfterTransition(this);
return false;
} else {
loadFragment(item.getItemId());
// Don't display the Settings item as checked
return item.getItemId() != R.id.nav_btn_settings;
}
}
@Override
......@@ -301,6 +317,11 @@ public class NavDrawerActivity extends BriarActivity implements
// Do nothing for now
}
private void setLockVisible(boolean visible) {
MenuItem item = navigation.getMenu().findItem(R.id.nav_btn_lock);
if (item != null) item.setVisible(visible);
}
@SuppressWarnings("ConstantConditions")
private void showExpiryWarning(ExpiryWarning expiry) {
int daysUntilExpiry = getDaysUntilExpiry();
......
......@@ -14,6 +14,10 @@ public interface NavDrawerController extends ActivityLifecycleController {
boolean isTransportRunning(TransportId transportId);
void isLockable(ResultHandler<Boolean> handler);
void lock();
void showExpiryWarning(ResultHandler<ExpiryWarning> handler);
void expiryWarningDismissed();
......
......@@ -3,6 +3,7 @@ package org.briarproject.briar.android.navdrawer;
import android.app.Activity;
import android.content.Context;
import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.Event;
......@@ -36,6 +37,7 @@ import static org.briarproject.briar.android.controller.BriarControllerImpl.DOZE
import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.NO;
import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.SHOW;
import static org.briarproject.briar.android.navdrawer.NavDrawerController.ExpiryWarning.UPDATE;
import static org.briarproject.briar.android.settings.SettingsFragment.PREF_SCREEN_LOCK;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
import static org.briarproject.briar.android.util.UiUtils.needsDozeWhitelisting;
......@@ -51,6 +53,7 @@ public class NavDrawerControllerImpl extends DbControllerImpl
private final PluginManager pluginManager;
private final SettingsManager settingsManager;
private final AccountManager accountManager;
private final EventBus eventBus;
private volatile TransportStateListener listener;
......@@ -58,10 +61,12 @@ public class NavDrawerControllerImpl extends DbControllerImpl
@Inject
NavDrawerControllerImpl(@DatabaseExecutor Executor dbExecutor,
LifecycleManager lifecycleManager, PluginManager pluginManager,
SettingsManager settingsManager, EventBus eventBus) {
SettingsManager settingsManager, AccountManager accountManager,
EventBus eventBus) {
super(dbExecutor, lifecycleManager);
this.pluginManager = pluginManager;
this.settingsManager = settingsManager;
this.accountManager = accountManager;
this.eventBus = eventBus;
}
......@@ -107,6 +112,25 @@ public class NavDrawerControllerImpl extends DbControllerImpl
() -> listener.stateUpdate(id, enabled));
}
@Override
public void isLockable(ResultHandler<Boolean> handler) {
runOnDbThread(() -> {
try {
Settings settings =
settingsManager.getSettings(SETTINGS_NAMESPACE);
boolean ask = settings.getBoolean(PREF_SCREEN_LOCK, false);
handler.onResult(ask);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@Override
public void lock() {
accountManager.setLocked(true);
}
@Override
public void showExpiryWarning(ResultHandler<ExpiryWarning> handler) {
if (!IS_DEBUG_BUILD && !IS_BETA_BUILD) {
......
......@@ -82,10 +82,12 @@ import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.bramble.util.StringUtils.join;
import static org.briarproject.briar.android.TestingConstants.FEATURE_FLAG_DARK_THEME;
import static org.briarproject.briar.android.TestingConstants.FEATURE_FLAG_PIN_LOCK;
import static org.briarproject.briar.android.TestingConstants.FEATURE_FLAG_SIGN_IN_REMINDER;
import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_RINGTONE;
import static org.briarproject.briar.android.navdrawer.NavDrawerActivity.INTENT_SIGN_OUT;
import static org.briarproject.briar.android.util.UiUtils.hasScreenLock;
import static org.briarproject.briar.api.android.AndroidNotificationManager.BLOG_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.CONTACT_CHANNEL_ID;
import static org.briarproject.briar.api.android.AndroidNotificationManager.FORUM_CHANNEL_ID;
......@@ -109,6 +111,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
public static final String BT_NAMESPACE = BluetoothConstants.ID.getString();
public static final String TOR_NAMESPACE = TorConstants.ID.getString();
public static final String LANGUAGE = "pref_key_language";
public static final String PREF_SCREEN_LOCK = "pref_key_lock";
public static final String NOTIFY_SIGN_IN = "pref_key_notify_sign_in";
public static final String TOR_LOCATION = "pref_key_tor_location";
......@@ -120,6 +123,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
private ListPreference enableBluetooth;
private ListPreference torNetwork;
private CheckBoxPreference torBlocked;
private CheckBoxPreference screenLock;
private CheckBoxPreference notifyPrivateMessages;
private CheckBoxPreference notifyGroupMessages;
private CheckBoxPreference notifyForumPosts;
......@@ -162,6 +166,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
setBlockedCountries();
CheckBoxPreference notifySignIn =
(CheckBoxPreference) findPreference(NOTIFY_SIGN_IN);
screenLock = (CheckBoxPreference) findPreference(PREF_SCREEN_LOCK);
notifyPrivateMessages = (CheckBoxPreference) findPreference(
"pref_key_notify_private_messages");
notifyGroupMessages = (CheckBoxPreference) findPreference(
......@@ -200,6 +205,12 @@ public class SettingsFragment extends PreferenceFragmentCompat
enableBluetooth.setOnPreferenceChangeListener(this);
torNetwork.setOnPreferenceChangeListener(this);
torBlocked.setOnPreferenceChangeListener(this);
if (getActivity() != null && hasScreenLock(getActivity())) {
screenLock.setVisible(FEATURE_FLAG_PIN_LOCK);
screenLock.setOnPreferenceChangeListener(this);
} else {
screenLock.setVisible(false);
}
if (SDK_INT >= 21) {
notifyLockscreen.setVisible(true);
notifyLockscreen.setOnPreferenceChangeListener(this);
......@@ -340,8 +351,10 @@ public class SettingsFragment extends PreferenceFragmentCompat
PREF_TOR_NETWORK_ALWAYS);
boolean torBlockedSetting =
torSettings.getBoolean(PREF_TOR_DISABLE_BLOCKED, true);
displaySettings(btSetting, torNetworkSetting,
torBlockedSetting);
boolean screenLockSetting =
settings.getBoolean(PREF_SCREEN_LOCK, false);
displaySettings(btSetting, torNetworkSetting, torBlockedSetting,
screenLockSetting);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
......@@ -349,11 +362,12 @@ public class SettingsFragment extends PreferenceFragmentCompat
}
private void displaySettings(boolean btSetting, int torNetworkSetting,
boolean torBlockedSetting) {
boolean torBlockedSetting, boolean screenLockSetting) {
listener.runOnUiThreadUnlessDestroyed(() -> {
enableBluetooth.setValue(Boolean.toString(btSetting));
torNetwork.setValue(Integer.toString(torNetworkSetting));
torBlocked.setChecked(torBlockedSetting);
screenLock.setChecked(screenLockSetting);
if (SDK_INT < 26) {
notifyPrivateMessages.setChecked(settings.getBoolean(
......@@ -414,6 +428,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
enableBluetooth.setEnabled(enabled);
torNetwork.setEnabled(enabled);
torBlocked.setEnabled(enabled);
screenLock.setEnabled(enabled);
notifyPrivateMessages.setEnabled(enabled);
notifyGroupMessages.setEnabled(enabled);
notifyForumPosts.setEnabled(enabled);
......@@ -478,6 +493,10 @@ public class SettingsFragment extends PreferenceFragmentCompat
} else if (preference == torBlocked) {
boolean torBlockedSetting = (Boolean) newValue;
storeTorBlockedSetting(torBlockedSetting);
} else if (preference == screenLock) {
Settings s = new Settings();
s.putBoolean(PREF_SCREEN_LOCK, (Boolean) newValue);
storeSettings(s);
} else if (preference == notifyPrivateMessages) {
Settings s = new Settings();
s.putBoolean(PREF_NOTIFY_PRIVATE, (Boolean) newValue);
......
......@@ -14,6 +14,7 @@ import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BaseActivity;
import org.briarproject.briar.android.login.OpenDatabaseActivity;
import org.briarproject.briar.android.login.SetupActivity;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import java.util.logging.Logger;
......@@ -44,7 +45,15 @@ public class SplashScreenActivity extends BaseActivity {
setContentView(R.layout.splash);
if (accountManager.hasDatabaseKey()) {
startActivity(new Intent(this, OpenDatabaseActivity.class));
Intent i;
if (accountManager.isLocked()) {
// The database is already open, so start main activity which
// will open the activity to unlock, then brings main to front.
i = new Intent(this, NavDrawerActivity.class);
} else {
i = new Intent(this, OpenDatabaseActivity.class);
}
startActivity(i);
finish();
} else {
new Handler().postDelayed(() -> {
......
......@@ -2,6 +2,7 @@ package org.briarproject.briar.android.util;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
......@@ -229,4 +230,13 @@ public class UiUtils {
return ContextCompat.getColor(ctx, color);
}
public static boolean hasScreenLock(Context ctx) {
KeyguardManager keyguardManager = (KeyguardManager) ctx
.getSystemService(Context.KEYGUARD_SERVICE);
if (keyguardManager == null || SDK_INT < 21) return false;
// check if there's a lock mechanism we can use, try to ignore SIM
return (SDK_INT < 23 && keyguardManager.isKeyguardSecure()) ||
(SDK_INT >= 23 && keyguardManager.isDeviceSecure());
}
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".android.login.UnlockActivity">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/image"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_margin="@dimen/margin_large"
android:src="@drawable/splash_screen"
app:layout_constraintBottom_toTopOf="@+id/is_locked"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread"
app:tint="?attr/colorControlNormal"/>
<TextView
android:id="@+id/is_locked"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_large"
android:gravity="center"
android:text="@string/lock_is_locked"
android:textSize="@dimen/text_size_xlarge"
app:layout_constraintBottom_toTopOf="@+id/unlock"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image"
app:layout_constraintVertical_chainStyle="spread"/>
<Button
android:id="@+id/unlock"
style="@style/BriarButton.Default"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_large"
android:text="@string/lock_unlock"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
......@@ -26,6 +26,11 @@
android:id="@+id/nav_btn_settings"
android:icon="@drawable/ic_settings_black_24dp"
android:title="@string/settings_button"/>
<item
android:id="@+id/nav_btn_lock"
android:icon="@drawable/startup_lock"
android:title="@string/lock_button"
android:visible="false"/>
<item
android:id="@+id/nav_btn_signout"
android:icon="@drawable/ic_signout"
......
......@@ -62,6 +62,7 @@
<string name="groups_button">Private Groups</string>
<string name="forums_button">Forums</string>
<string name="blogs_button">Blogs</string>
<string name="lock_button">Lock App</string>
<string name="settings_button">Settings</string>
<string name="sign_out_button">Sign Out</string>
......@@ -356,6 +357,8 @@
<!-- Settings Security and Panic -->
<string name="security_settings_title">Security</string>
<string name="pref_lock_title">Screen Lock</string>
<string name="pref_lock_summary">Lock app access with Android screen lock or fingerprint</string>
<string name="change_password">Change password</string>
<string name="current_password">Current password</string>
<string name="choose_new_password">New password</string>
......@@ -444,4 +447,10 @@
<string name="permission_camera_denied_toast">Camera permission was not granted</string>
<string name="qr_code">QR code</string>
<string name="show_qr_code_fullscreen">Show QR code fullscreen</string>
<!-- App Locking -->
<string name="lock_unlock">Unlock Briar</string>
<string name="lock_is_locked">Briar is locked</string>
<string name="lock_tap_to_unlock">Tap to unlock</string>
</resources>
......@@ -56,6 +56,13 @@
android:layout="@layout/preferences_category"
android:title="@string/security_settings_title">
<CheckBoxPreference
android:key="pref_key_lock"
android:persistent="false"
android:summary="@string/pref_lock_summary"
android:title="@string/pref_lock_title"
android:visibility="gone"/>
<Preference
android:key="pref_key_change_password"
android:title="@string/change_password">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment