Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ android {
implementation "androidx.media3:media3-ui:$media3_version"
implementation "androidx.media3:media3-datasource-okhttp:$media3_version"

implementation "androidx.biometric:biometric:1.4.0-alpha05"
implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.appcompat:appcompat:1.7.1"
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@
</intent-filter>
</activity>

<activity android:name=".ui.LockedActivity"
android:launchMode="singleTop"
android:theme="@style/App.Theme.Splash" />

<activity
android:name=".ui.account.AccountsActivity"
android:launchMode="singleTop" />
Expand Down
29 changes: 29 additions & 0 deletions app/src/main/java/com/seafile/seadroid2/SeadroidApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import androidx.annotation.BoolRes;
import androidx.annotation.IntegerRes;
import androidx.annotation.StringRes;
import androidx.biometric.BiometricManager;
import androidx.work.Configuration;
import androidx.work.WorkManager;

Expand All @@ -19,6 +20,7 @@
import com.seafile.seadroid2.framework.util.SafeLogs;
import com.seafile.seadroid2.preferences.Settings;
import com.seafile.seadroid2.provider.DocumentCache;
import com.seafile.seadroid2.ui.LockedActivity;
import com.seafile.seadroid2.ui.camera_upload.AlbumBackupAdapterBridge;

import io.reactivex.exceptions.UndeliverableException;
Expand All @@ -28,13 +30,16 @@

public class SeadroidApplication extends Application {
private static Context context = null;
private static boolean isLocked = false;

@Override
public void onCreate() {
super.onCreate();

context = this;

lockIfNeeded();

//init xlog in com.seafile.seadroid2.provider.SeafileProvider#onCreate()
// SLogs.init();

Expand Down Expand Up @@ -111,4 +116,28 @@ public static DocumentCache getDocumentCache() {
private static SeadroidApplication getApplication() {
return (SeadroidApplication) getAppContext();
}

public static boolean isLocked() {
return isLocked;
}

public static boolean canLock() {
// This allows the lock to be removed by, e.g., removing all biometrics/passwords,
// but that's already a privileged action anyway.
// In the future, it may be worthwhile looking into clearing auth data if passwords/biometrics
// change, but this is a good balance of security vs convenience.
return BiometricManager.from(context).canAuthenticate(LockedActivity.BIOMETRIC_REQS)
== BiometricManager.BIOMETRIC_SUCCESS;
}

public static void lockIfNeeded() {
// Only lock if we're allowed to and the device supports it.
if (Settings.BIOMETRIC_LOCK_SWITCH.queryValue() && canLock()) {
isLocked = true;
}
}

public static void unlock() {
isLocked = false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import android.app.Activity;
import android.app.Application;
import android.content.Intent;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.seafile.seadroid2.SeadroidApplication;
import com.seafile.seadroid2.ui.LockedActivity;
import com.seafile.seadroid2.view.webview.PreloadWebView;


Expand All @@ -18,6 +21,18 @@ public class ActivityMonitor implements Application.ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
PreloadWebView.getInstance().preload();

// Prevent any normal activities from being entered while the app is locked.
if (SeadroidApplication.isLocked() && !(activity instanceof LockedActivity)) {
Intent intent = new Intent(activity, LockedActivity.class);
// Allow returning back to this initial activity.
intent.putExtra("TARGET_INTENT", activity.getIntent());
// Prevent back button / returning some other way.
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);

activity.startActivity(intent);
activity.finish();
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public Settings() {
public static final SettingsLiveData<Boolean> SETTINGS_GESTURE = new BooleanSettingLiveData(R.string.pref_key_settings_gesture_lock, R.bool.pref_default_true);
public static final SettingsLiveData<Long> SETTINGS_GESTURE_LOCK_TIMESTAMP = new LongSettingLiveData(R.string.pref_key_settings_gesture_lock_timestamp, R.string.pref_default_value_key_gesture_lock_timestamp);

public static final SettingsLiveData<Boolean> BIOMETRIC_LOCK_SWITCH = new BooleanSettingLiveData(R.string.pref_key_settings_biometric_lock, R.bool.pref_default_false);

//////////////////
/// user settings
//////////////////
Expand Down
79 changes: 79 additions & 0 deletions app/src/main/java/com/seafile/seadroid2/ui/LockedActivity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.seafile.seadroid2.ui;

import android.content.Intent;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import androidx.core.splashscreen.SplashScreen;

import com.seafile.seadroid2.R;
import com.seafile.seadroid2.SeadroidApplication;

import java.util.concurrent.Executor;

public class LockedActivity extends AppCompatActivity {
public static int BIOMETRIC_REQS = BiometricManager.Authenticators.BIOMETRIC_STRONG
| BiometricManager.Authenticators.DEVICE_CREDENTIAL;


@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Show a "fake" splash screen for better UX.
SplashScreen splashScreen = SplashScreen.installSplashScreen(this);
splashScreen.setKeepOnScreenCondition(() -> true);

biometricPrompt();
}

private void biometricPrompt() {
Executor executor = ContextCompat.getMainExecutor(this);
BiometricPrompt biometricPrompt = new BiometricPrompt(this, executor,
new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);

unlock();
}

@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);

// The user exited, or something's wrong with the system.
// The only thing we can do at this point is exit.
finish();
}
});

BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.biometric_prompt_title))
.setAllowedAuthenticators(BIOMETRIC_REQS)
.build();

biometricPrompt.authenticate(promptInfo);
}

private void unlock() {
SeadroidApplication.unlock();

// Use the supplied next activity, if applicable.
Intent targetIntent = getIntent().getParcelableExtra("TARGET_INTENT");
if (targetIntent == null) {
targetIntent = new Intent(LockedActivity.this, SplashActivity.class);
}

// Prevent back button / returning some other way.
targetIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);

startActivity(targetIntent);
finish();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import androidx.core.splashscreen.SplashScreen;

import com.blankj.utilcode.util.ActivityUtils;
import com.seafile.seadroid2.SeadroidApplication;
import com.seafile.seadroid2.account.Account;
import com.seafile.seadroid2.account.SupportAccountManager;
import com.seafile.seadroid2.compat.AppCompatKt;
Expand Down Expand Up @@ -47,6 +48,14 @@ protected void onCreate(Bundle savedInstanceState) {
// setContentView(R.layout.activity_splash);
splashScreen.setKeepOnScreenCondition(() -> true);

// If the screen is locked, we'll be intercepted, then
// the splashscreen will be re-launched after the lock is passed.
// Stop here to prevent UI bugs caused by multiple instances of
// the lockscreen being opened.
if (SeadroidApplication.isLocked()) {
return;
}

dataMigrationLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult o) {
Expand Down
23 changes: 16 additions & 7 deletions app/src/main/java/com/seafile/seadroid2/ui/main/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import android.os.IBinder;
import android.text.TextUtils;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.widget.LinearLayout;

import androidx.activity.OnBackPressedCallback;
Expand Down Expand Up @@ -122,6 +124,8 @@ protected void onCreate(Bundle savedInstanceState) {
registerComponent();

requestServerInfo(true);

startWatching();
}

private void applyEdgeToEdgeInsets() {
Expand Down Expand Up @@ -234,6 +238,18 @@ public static void navToThis(Context context, String repo_id, String repo_name,
context.startActivity(intent);
}

@Override
public void onResume() {
super.onResume();

// This part of the view sometimes gets focused, causing
// the UI to look really bad.
View child = binding.pager.getChildAt(0);
if (child != null) {
child.setDefaultFocusHighlightEnabled(false);
}
}

@Override
public void onRestart() {
super.onRestart();
Expand All @@ -245,13 +261,6 @@ public void onRestart() {
// }
}

@Override
protected void onStart() {
super.onStart();

startWatching();
}

@Override
protected void onDestroy() {
NetworkUtils.unregisterNetworkStatusChangedListener(onNetworkStatusChangedListener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;

import com.blankj.utilcode.util.AppUtils;
import com.blankj.utilcode.util.CollectionUtils;
Expand Down Expand Up @@ -129,6 +130,8 @@ public class TabSettings2Fragment extends RenameSharePreferenceFragmentCompat {
private Preference mTransferUploadState;
private Preference cacheLocationPref;

private TextSwitchPreference mBiometricLockSwitch;

public static TabSettings2Fragment newInstance() {
return new TabSettings2Fragment();
}
Expand Down Expand Up @@ -272,6 +275,8 @@ private void initPref() {

initCachePref();

initSecurityPref();

initAboutPref();
}

Expand All @@ -294,6 +299,23 @@ private void initSignOutPref() {
onPreferenceSignOutClicked();
return true;
});
}

private void initSecurityPref() {
mBiometricLockSwitch = findPreference(getString(R.string.pref_key_settings_biometric_lock));
mBiometricLockSwitch.setOnPreferenceChangeListener((preference, newValue) -> {
Settings.BIOMETRIC_LOCK_SWITCH.putValue((Boolean) newValue);
return true;
});

// This is right below biometric lock, and needs to be specially
// styled depending on its visibility.
TextTitleSummaryPreference clearPassword = findPreference(getString(R.string.pref_key_security_clear_password));

// Hide when unsupported by the user's device.
boolean canLock = SeadroidApplication.canLock();
mBiometricLockSwitch.setVisible(canLock);
clearPassword.setRadiusPosition(canLock ? RadiusPositionEnum.BOTTOM : RadiusPositionEnum.ALL);

//clear pwd
findPreference(getString(R.string.pref_key_security_clear_password)).setOnPreferenceClickListener(preference -> {
Expand Down Expand Up @@ -745,6 +767,16 @@ public void onChanged(String s) {
findPreference(getString(R.string.pref_key_cache_info)).setSummary(s);
}
});

//////////////////
/// security
//////////////////
Settings.BIOMETRIC_LOCK_SWITCH.observe(getViewLifecycleOwner(), new Observer<Boolean>() {
@Override
public void onChanged(Boolean aBoolean) {
mBiometricLockSwitch.setChecked(aBoolean);
}
});
}

private void initWorkerBusObserver() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,17 @@ public int getLayoutId() {
return R.layout.layout_pref_title_switch;
}

private MaterialSwitch materialSwitch;

@Override
public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
super.onBindViewHolder(holder);

materialSwitch = (MaterialSwitch) holder.findViewById(android.R.id.switch_widget);
materialSwitch.setClickable(false);
}
MaterialSwitch switchView = (MaterialSwitch) holder.findViewById(android.R.id.switch_widget);

public void setChecked(boolean checked) {
super.setChecked(checked);
// Ensure the switch view can't steal onclick events.
// If it does, it'll visually change but won't call any corresponding logic.
switchView.setClickable(false);

if (materialSwitch != null) {
materialSwitch.setChecked(checked);
}
// Propagate state to the view.
switchView.setChecked(isChecked());
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/donottranslate_prefs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<string name="pref_key_file_list_sort_folder_first">key_file_list_sort_folder_first</string>
<string name="pref_key_settings_gesture_lock">key_settings_gesture_lock</string>
<string name="pref_key_settings_gesture_lock_timestamp">key_settings_gesture_lock_timestamp</string>
<string name="pref_key_settings_biometric_lock">key_settings_biometric_lock</string>

<!-- account -->
<string name="pref_key_user_info">key_user_info</string>
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,10 @@
<string name="gesture_lock">Gesture Lock</string>
<string name="gesture_lock_on">Gesture lock on</string>
<string name="gesture_lock_off">Gesture lock off</string>
<string name="biometric_lock">Biometric lock</string>
<string name="biometric_lock_on">Use biometrics to unlock the app</string>
<string name="biometric_lock_off">The application is not protected by biometrics</string>
<string name="biometric_prompt_title">Biometric Login</string>
<string name="lockscreen_access_pattern_hint">Please draw Gesture Lock Pattern</string>
<string name="lockscreen_access_pattern_start">Start to draw</string>
<string name="lockscreen_access_pattern_cleared">Pattern cleared</string>
Expand Down
Loading