From ccca9c44006724ddfeec85f0b2b51b6a0770ca91 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 18 Jul 2018 14:37:39 +0200 Subject: [PATCH] #77 Add read-only mode --- app/build.gradle | 2 +- .../keepass/activities/EntryActivity.java | 13 +- .../keepass/activities/GroupActivity.java | 82 ++++++------ .../activities/IntentBuildLauncher.java | 7 + .../keepass/activities/ListNodesActivity.java | 18 ++- .../keepass/activities/ReadOnlyHelper.java | 61 +++++++++ .../keepass/password/PasswordActivity.java | 124 ++++++++++++++---- .../settings/NestedSettingsFragment.java | 72 ++++++---- .../keepass/settings/PreferencesUtil.java | 19 +++ .../keepass/settings/SettingsActivity.java | 26 +++- .../com/kunzisoft/keepass/utils/MenuUtil.java | 8 +- .../keepass/view/AddNodeButtonView.java | 14 ++ .../res/drawable/ic_read_only_white_24dp.xml | 19 +++ .../res/drawable/ic_read_write_white_24dp.xml | 25 ++++ app/src/main/res/menu/open_file.xml | 27 ++++ app/src/main/res/values/donottranslate.xml | 4 + app/src/main/res/values/strings.xml | 6 + .../main/res/xml/application_preferences.xml | 5 + art/ic_read_only.svg | 92 +++++++++++++ art/ic_read_write.svg | 92 +++++++++++++ 20 files changed, 605 insertions(+), 111 deletions(-) create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/IntentBuildLauncher.java create mode 100644 app/src/main/java/com/kunzisoft/keepass/activities/ReadOnlyHelper.java create mode 100644 app/src/main/res/drawable/ic_read_only_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_read_write_white_24dp.xml create mode 100644 app/src/main/res/menu/open_file.xml create mode 100644 art/ic_read_only.svg create mode 100644 art/ic_read_write.svg diff --git a/app/build.gradle b/app/build.gradle index 8d9385cdd..45d5d3c9f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ android { defaultConfig { applicationId "com.kunzisoft.keepass" - minSdkVersion 15 + minSdkVersion 14 targetSdkVersion 27 versionCode = 14 versionName = "2.5.0.0beta14" diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java index ef5da57ed..e77333e69 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.java @@ -83,10 +83,11 @@ public class EntryActivity extends LockingHideActivity { private ClipboardHelper clipboardHelper; private boolean firstLaunchOfActivity; - public static void launch(Activity act, PwEntry pw) { + public static void launch(Activity act, PwEntry pw, boolean readOnly) { if (LockingActivity.checkTimeIsAllowedOrFinish(act)) { Intent intent = new Intent(act, EntryActivity.class); intent.putExtra(KEY_ENTRY, Types.UUIDtoBytes(pw.getUUID())); + ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly); act.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE); } } @@ -104,13 +105,15 @@ public class EntryActivity extends LockingHideActivity { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true); + readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, getIntent()); + Database db = App.getDB(); // Likely the app has been killed exit the activity if ( ! db.getLoaded() ) { finish(); return; } - readOnly = db.isReadOnly(); + readOnly = db.isReadOnly() || readOnly; mShowPassword = !PreferencesUtil.isPasswordMask(this); @@ -230,6 +233,12 @@ public class EntryActivity extends LockingHideActivity { firstLaunchOfActivity = false; } + @Override + protected void onSaveInstanceState(Bundle outState) { + ReadOnlyHelper.onSaveInstanceState(outState, readOnly); + super.onSaveInstanceState(outState); + } + /** * Check and display learning views * Displays the explanation for copying a field and editing an entry diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java index d90d8d8bf..4ccbfe586 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.java @@ -80,6 +80,8 @@ import com.kunzisoft.keepass.view.AddNodeButtonView; import net.cachapa.expandablelayout.ExpandableLayout; +import static com.kunzisoft.keepass.activities.ReadOnlyHelper.READ_ONLY_DEFAULT; + public class GroupActivity extends ListNodesActivity implements GroupEditDialogFragment.EditGroupListener, IconPickerDialogFragment.IconPickerListener, @@ -88,6 +90,8 @@ public class GroupActivity extends ListNodesActivity private static final String TAG = GroupActivity.class.getName(); + protected static final String GROUP_ID_KEY = "GROUP_ID_KEY"; + private Toolbar toolbar; private ExpandableLayout toolbarPasteExpandableLayout; @@ -99,7 +103,6 @@ public class GroupActivity extends ListNodesActivity protected boolean addGroupEnabled = false; protected boolean addEntryEnabled = false; protected boolean isRoot = false; - protected boolean readOnly = false; private static final String OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"; private static final String NODE_TO_COPY_KEY = "NODE_TO_COPY_KEY"; @@ -107,64 +110,67 @@ public class GroupActivity extends ListNodesActivity private PwGroup oldGroupToUpdate; private PwNode nodeToCopy; private PwNode nodeToMove; - - public static void launch(Activity act) { + + // After a database creation + public static void launch(Activity act) { + launch(act, READ_ONLY_DEFAULT); + } + + public static void launch(Activity act, boolean readOnly) { startRecordTime(act); - launch(act, null); + launch(act, null, readOnly); } - public static void launch(Activity act, PwGroup group) { - if (checkTimeIsAllowedOrFinish(act)) { - Intent intent = new Intent(act, GroupActivity.class); + private static void buildAndLaunchIntent(Activity activity, PwGroup group, boolean readOnly, + IntentBuildLauncher intentBuildLauncher) { + if (checkTimeIsAllowedOrFinish(activity)) { + Intent intent = new Intent(activity, GroupActivity.class); if (group != null) { intent.putExtra(GROUP_ID_KEY, group.getId()); } - act.startActivityForResult(intent, 0); + ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly); + intentBuildLauncher.startActivityForResult(intent); } } - public static void launchForKeyboardResult(Activity act) { + public static void launch(Activity activity, PwGroup group, boolean readOnly) { + buildAndLaunchIntent(activity, group, readOnly, + (intent) -> activity.startActivityForResult(intent, 0)); + } + + public static void launchForKeyboardResult(Activity act, boolean readOnly) { startRecordTime(act); - launchForKeyboardResult(act, null); + launchForKeyboardResult(act, null, readOnly); } - public static void launchForKeyboardResult(Activity act, PwGroup group) { - // TODO remove - if (checkTimeIsAllowedOrFinish(act)) { - Intent intent = new Intent(act, GroupActivity.class); - if (group != null) { - intent.putExtra(GROUP_ID_KEY, group.getId()); - } + public static void launchForKeyboardResult(Activity activity, PwGroup group, boolean readOnly) { + // TODO implement pre search to directly open the direct group + buildAndLaunchIntent(activity, group, readOnly, (intent) -> { EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent); - act.startActivityForResult(intent, EntrySelectionHelper.ENTRY_SELECTION_RESPONSE_REQUEST_CODE); - } + activity.startActivityForResult(intent, EntrySelectionHelper.ENTRY_SELECTION_RESPONSE_REQUEST_CODE); + }); } - @RequiresApi(api = Build.VERSION_CODES.O) - public static void launchForAutofillResult(Activity act, AssistStructure assistStructure) { + public static void launchForAutofillResult(Activity act, AssistStructure assistStructure, boolean readOnly) { if ( assistStructure != null ) { startRecordTime(act); - launchForAutofillResult(act, null, assistStructure); + launchForAutofillResult(act, null, assistStructure, readOnly); } else { - launch(act); + launch(act, readOnly); } } @RequiresApi(api = Build.VERSION_CODES.O) - public static void launchForAutofillResult(Activity act, PwGroup group, AssistStructure assistStructure) { - // TODO remove + public static void launchForAutofillResult(Activity activity, PwGroup group, AssistStructure assistStructure, boolean readOnly) { + // TODO implement pre search to directly open the direct group if ( assistStructure != null ) { - if (checkTimeIsAllowedOrFinish(act)) { - Intent intent = new Intent(act, GroupActivity.class); - if (group != null) { - intent.putExtra(GROUP_ID_KEY, group.getId()); - } + buildAndLaunchIntent(activity, group, readOnly, (intent) -> { AutofillHelper.addAssistStructureExtraInIntent(intent, assistStructure); - act.startActivityForResult(intent, AutofillHelper.AUTOFILL_RESPONSE_REQUEST_CODE); - } + activity.startActivityForResult(intent, AutofillHelper.AUTOFILL_RESPONSE_REQUEST_CODE); + }); } else { - launch(act, group); + launch(activity, group, readOnly); } } @@ -255,7 +261,7 @@ public class GroupActivity extends ListNodesActivity } Database db = App.getDB(); - readOnly = db.isReadOnly(); + readOnly = db.isReadOnly() || readOnly; // Force read only if the database is like that PwGroup root = db.getPwDatabase().getRootGroup(); Log.w(TAG, "Creating tree view"); @@ -268,7 +274,7 @@ public class GroupActivity extends ListNodesActivity if (currentGroup != null) { addGroupEnabled = !readOnly; - addEntryEnabled = !readOnly; // TODO consultation mode + addEntryEnabled = !readOnly; isRoot = (currentGroup == root); if (!currentGroup.allowAddEntryIfIsRoot()) addEntryEnabled = !isRoot && addEntryEnabled; @@ -496,7 +502,8 @@ public class GroupActivity extends ListNodesActivity // If no node, show education to add new one if (listNodesFragment != null && listNodesFragment.isEmpty()) { - if (!PreferencesUtil.isEducationNewNodePerformed(this)) { + if (!PreferencesUtil.isEducationNewNodePerformed(this) + && addNodeButtonView.isVisible()) { TapTargetView.showFor(this, TapTarget.forView(findViewById(R.id.add_button), @@ -633,7 +640,8 @@ public class GroupActivity extends ListNodesActivity MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.search, menu); - inflater.inflate(R.menu.database_master_key, menu); + if (!readOnly) + inflater.inflate(R.menu.database_master_key, menu); inflater.inflate(R.menu.database_lock, menu); // Get the SearchView and set the searchable configuration diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/IntentBuildLauncher.java b/app/src/main/java/com/kunzisoft/keepass/activities/IntentBuildLauncher.java new file mode 100644 index 000000000..f7eddb8be --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/IntentBuildLauncher.java @@ -0,0 +1,7 @@ +package com.kunzisoft.keepass.activities; + +import android.content.Intent; + +public interface IntentBuildLauncher { + void startActivityForResult(Intent intent); +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesActivity.java b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesActivity.java index 6779949c7..3d3897433 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/activities/ListNodesActivity.java @@ -41,9 +41,9 @@ import com.kunzisoft.keepass.database.PwNode; import com.kunzisoft.keepass.database.SortNodeEnum; import com.kunzisoft.keepass.dialogs.AssignMasterKeyDialogFragment; import com.kunzisoft.keepass.dialogs.SortDialogFragment; -import com.kunzisoft.keepass.selection.EntrySelectionHelper; import com.kunzisoft.keepass.lock.LockingActivity; import com.kunzisoft.keepass.password.AssignPasswordHelper; +import com.kunzisoft.keepass.selection.EntrySelectionHelper; import com.kunzisoft.keepass.utils.MenuUtil; public abstract class ListNodesActivity extends LockingActivity @@ -51,14 +51,14 @@ public abstract class ListNodesActivity extends LockingActivity NodeAdapter.NodeClickCallback, SortDialogFragment.SortSelectionListener { - protected static final String GROUP_ID_KEY = "GROUP_ID_KEY"; - protected static final String LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"; protected ListNodesFragment listNodesFragment; protected PwGroup mCurrentGroup; protected TextView groupNameView; + protected boolean readOnly; + protected boolean entrySelectionMode; protected AutofillHelper autofillHelper; @@ -76,6 +76,8 @@ public abstract class ListNodesActivity extends LockingActivity return; } + readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, getIntent()); + invalidateOptionsMenu(); mCurrentGroup = retrieveCurrentGroup(savedInstanceState); @@ -89,6 +91,12 @@ public abstract class ListNodesActivity extends LockingActivity } } + @Override + protected void onSaveInstanceState(Bundle outState) { + ReadOnlyHelper.onSaveInstanceState(outState, readOnly); + super.onSaveInstanceState(outState); + } + protected abstract PwGroup retrieveCurrentGroup(@Nullable Bundle savedInstanceState); @Override @@ -151,7 +159,7 @@ public abstract class ListNodesActivity extends LockingActivity switch ( item.getItemId() ) { default: // Check the time lock before launching settings - MenuUtil.onDefaultMenuOptionsItemSelected(this, item, true); + MenuUtil.onDefaultMenuOptionsItemSelected(this, item, readOnly, true); return super.onOptionsItemSelected(item); } } @@ -193,7 +201,7 @@ public abstract class ListNodesActivity extends LockingActivity openGroup((PwGroup) node); break; case ENTRY: - EntryActivity.launch(this, (PwEntry) node); + EntryActivity.launch(this, (PwEntry) node, readOnly); break; } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ReadOnlyHelper.java b/app/src/main/java/com/kunzisoft/keepass/activities/ReadOnlyHelper.java new file mode 100644 index 000000000..78164c69a --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/ReadOnlyHelper.java @@ -0,0 +1,61 @@ +package com.kunzisoft.keepass.activities; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import com.kunzisoft.keepass.settings.PreferencesUtil; + +public class ReadOnlyHelper { + + public static final String READ_ONLY_KEY = "READ_ONLY_KEY"; + + public static final boolean READ_ONLY_DEFAULT = false; + + public static boolean retrieveReadOnlyFromInstanceStateOrPreference(Context context, Bundle savedInstanceState) { + boolean readOnly; + if (savedInstanceState != null + && savedInstanceState.containsKey(READ_ONLY_KEY)) { + readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY); + } else { + readOnly = PreferencesUtil.enableReadOnlyDatabase(context); + } + return readOnly; + } + + public static boolean retrieveReadOnlyFromInstanceStateOrArguments(Bundle savedInstanceState, Bundle arguments) { + boolean readOnly = READ_ONLY_DEFAULT; + if (savedInstanceState != null + && savedInstanceState.containsKey(READ_ONLY_KEY)) { + readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY); + } else if (arguments != null + && arguments.containsKey(READ_ONLY_KEY)) { + readOnly = arguments.getBoolean(READ_ONLY_KEY); + } + return readOnly; + } + + public static boolean retrieveReadOnlyFromInstanceStateOrIntent(Bundle savedInstanceState, Intent intent) { + boolean readOnly = READ_ONLY_DEFAULT; + if (savedInstanceState != null + && savedInstanceState.containsKey(READ_ONLY_KEY)) { + readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY); + } else { + if (intent != null) + readOnly = intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT); + } + return readOnly; + } + + public static void putReadOnlyInIntent(Intent intent, boolean readOnly) { + intent.putExtra(READ_ONLY_KEY, readOnly); + } + + public static void putReadOnlyInBundle(Bundle bundle, boolean readOnly) { + bundle.putBoolean(READ_ONLY_KEY, readOnly); + } + + public static void onSaveInstanceState(Bundle outState, boolean readOnly) { + outState.putBoolean(READ_ONLY_KEY, readOnly); + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java b/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java index 249977583..12ebc0153 100644 --- a/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/password/PasswordActivity.java @@ -55,9 +55,9 @@ import android.widget.Toast; import com.getkeepsafe.taptargetview.TapTarget; import com.getkeepsafe.taptargetview.TapTargetView; import com.kunzisoft.keepass.R; +import com.kunzisoft.keepass.activities.ReadOnlyHelper; import com.kunzisoft.keepass.activities.GroupActivity; -import com.kunzisoft.keepass.selection.EntrySelectionHelper; -import com.kunzisoft.keepass.lock.LockingActivity; +import com.kunzisoft.keepass.activities.IntentBuildLauncher; import com.kunzisoft.keepass.app.App; import com.kunzisoft.keepass.autofill.AutofillHelper; import com.kunzisoft.keepass.compat.ClipDataCompat; @@ -69,6 +69,8 @@ import com.kunzisoft.keepass.fileselect.KeyFileHelper; import com.kunzisoft.keepass.fingerprint.FingerPrintAnimatedVector; import com.kunzisoft.keepass.fingerprint.FingerPrintExplanationDialog; import com.kunzisoft.keepass.fingerprint.FingerPrintHelper; +import com.kunzisoft.keepass.lock.LockingActivity; +import com.kunzisoft.keepass.selection.EntrySelectionHelper; import com.kunzisoft.keepass.settings.PreferencesUtil; import com.kunzisoft.keepass.stylish.StylishActivity; import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment; @@ -124,6 +126,8 @@ public class PasswordActivity extends StylishActivity private CompoundButton checkboxDefaultDatabaseView; private CompoundButton.OnCheckedChangeListener enableButtonOncheckedChangeListener; + private boolean readOnly; + private DefaultCheckChange defaultCheckChange; private ValidateButtonViewClickListener validateButtonViewClickListener; @@ -139,16 +143,23 @@ public class PasswordActivity extends StylishActivity } public static void launch( - Activity act, + Activity activity, String fileName, String keyFile) throws FileNotFoundException { verifyFileNameUriFromLaunch(fileName); - Intent intent = new Intent(act, PasswordActivity.class); + buildAndLaunchIntent(activity, fileName, keyFile, (intent) -> { + // only to avoid visible flickering when redirecting + activity.startActivityForResult(intent, RESULT_CANCELED); + }); + } + + private static void buildAndLaunchIntent(Activity activity, String fileName, String keyFile, + IntentBuildLauncher intentBuildLauncher) { + Intent intent = new Intent(activity, PasswordActivity.class); intent.putExtra(UriIntentInitTask.KEY_FILENAME, fileName); intent.putExtra(UriIntentInitTask.KEY_KEYFILE, keyFile); - // only to avoid visible flickering when redirecting - act.startActivityForResult(intent, RESULT_CANCELED); + intentBuildLauncher.startActivityForResult(intent); } public static void launchForKeyboardResult( @@ -158,17 +169,16 @@ public class PasswordActivity extends StylishActivity } public static void launchForKeyboardResult( - Activity act, + Activity activity, String fileName, String keyFile) throws FileNotFoundException { verifyFileNameUriFromLaunch(fileName); - Intent intent = new Intent(act, PasswordActivity.class); - intent.putExtra(UriIntentInitTask.KEY_FILENAME, fileName); - intent.putExtra(UriIntentInitTask.KEY_KEYFILE, keyFile); - EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent); - // only to avoid visible flickering when redirecting - act.startActivityForResult(intent, EntrySelectionHelper.ENTRY_SELECTION_RESPONSE_REQUEST_CODE); + buildAndLaunchIntent(activity, fileName, keyFile, (intent) -> { + EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent); + // only to avoid visible flickering when redirecting + activity.startActivityForResult(intent, EntrySelectionHelper.ENTRY_SELECTION_RESPONSE_REQUEST_CODE); + }); } @RequiresApi(api = Build.VERSION_CODES.O) @@ -181,20 +191,19 @@ public class PasswordActivity extends StylishActivity @RequiresApi(api = Build.VERSION_CODES.O) public static void launchForAutofillResult( - Activity act, + Activity activity, String fileName, String keyFile, AssistStructure assistStructure) throws FileNotFoundException { verifyFileNameUriFromLaunch(fileName); if ( assistStructure != null ) { - Intent intent = new Intent(act, PasswordActivity.class); - intent.putExtra(UriIntentInitTask.KEY_FILENAME, fileName); - intent.putExtra(UriIntentInitTask.KEY_KEYFILE, keyFile); - AutofillHelper.addAssistStructureExtraInIntent(intent, assistStructure); - act.startActivityForResult(intent, AutofillHelper.AUTOFILL_RESPONSE_REQUEST_CODE); + buildAndLaunchIntent(activity, fileName, keyFile, (intent) -> { + AutofillHelper.addAssistStructureExtraInIntent(intent, assistStructure); + activity.startActivityForResult(intent, AutofillHelper.AUTOFILL_RESPONSE_REQUEST_CODE); + }); } else { - launch(act, fileName, keyFile); + launch(activity, fileName, keyFile); } } @@ -276,6 +285,8 @@ public class PasswordActivity extends StylishActivity checkboxKeyfileView = findViewById(R.id.keyfile_checkox); checkboxDefaultDatabaseView = findViewById(R.id.default_database); + readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState); + View browseView = findViewById(R.id.browse_button); keyFileHelper = new KeyFileHelper(PasswordActivity.this); browseView.setOnClickListener(keyFileHelper.getOpenFileOnClickViewListener()); @@ -326,8 +337,6 @@ public class PasswordActivity extends StylishActivity autofillHelper = new AutofillHelper(); autofillHelper.retrieveAssistStructure(getIntent()); } - - checkAndPerformedEducation(); } @Override @@ -380,11 +389,17 @@ public class PasswordActivity extends StylishActivity .execute(getIntent()); } + @Override + protected void onSaveInstanceState(Bundle outState) { + ReadOnlyHelper.onSaveInstanceState(outState, readOnly); + super.onSaveInstanceState(outState); + } + /** * Check and display learning views * Displays the explanation for a database opening with fingerprints if available */ - private void checkAndPerformedEducation() { + private void checkAndPerformedEducation(Menu menu) { if (PreferencesUtil.isEducationScreensEnabled(this)) { if (!PreferencesUtil.isEducationUnlockPerformed(this)) { @@ -414,7 +429,39 @@ public class PasswordActivity extends StylishActivity } }); // TODO make a period for donation - PreferencesUtil.saveEducationPreference(PasswordActivity.this, R.string.education_unlock_key); + PreferencesUtil.saveEducationPreference(PasswordActivity.this, + R.string.education_unlock_key); + + } else if (!PreferencesUtil.isEducationReadOnlyPerformed(this)) { + + try { + TapTargetView.showFor(this, + TapTarget.forToolbarMenuItem(toolbar, R.id.menu_open_file_read_mode_key, + getString(R.string.education_read_only_title), + getString(R.string.education_read_only_summary)) + .textColorInt(Color.WHITE) + .tintTarget(true) + .cancelable(true), + new TapTargetView.Listener() { + @Override + public void onTargetClick(TapTargetView view) { + super.onTargetClick(view); + MenuItem editItem = menu.findItem(R.id.menu_open_file_read_mode_key); + onOptionsItemSelected(editItem); + } + + @Override + public void onOuterCircleClick(TapTargetView view) { + super.onOuterCircleClick(view); + view.dismiss(false); + } + }); + PreferencesUtil.saveEducationPreference(this, + R.string.education_read_only_key); + } catch (Exception e) { + // If icon not visible + Log.w(TAG, "Can't performed education for entry's edition"); + } } } } @@ -953,14 +1000,14 @@ public class PasswordActivity extends StylishActivity if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { assistStructure = autofillHelper.getAssistStructure(); if (assistStructure != null) { - GroupActivity.launchForAutofillResult(PasswordActivity.this, assistStructure); + GroupActivity.launchForAutofillResult(PasswordActivity.this, assistStructure, readOnly); } } if (assistStructure == null) { if (entrySelectionMode) { - GroupActivity.launchForKeyboardResult(PasswordActivity.this); + GroupActivity.launchForKeyboardResult(PasswordActivity.this, readOnly); } else { - GroupActivity.launch(PasswordActivity.this); + GroupActivity.launch(PasswordActivity.this, readOnly); } } } @@ -968,16 +1015,35 @@ public class PasswordActivity extends StylishActivity @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); + // Read menu + inflater.inflate(R.menu.open_file, menu); + changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key)); + MenuUtil.defaultMenuInflater(inflater, menu); + + // Fingerprint menu if (!fingerprintMustBeConfigured && prefsNoBackup.contains(getPreferenceKeyValue()) ) inflater.inflate(R.menu.fingerprint, menu); super.onCreateOptionsMenu(menu); + // Show education views + new Handler().post(() -> checkAndPerformedEducation(menu)); + return true; } + private void changeOpenFileReadIcon(MenuItem togglePassword) { + if ( readOnly ) { + togglePassword.setTitle(R.string.menu_file_selection_read_only); + togglePassword.setIcon(R.drawable.ic_read_only_white_24dp); + } else { + togglePassword.setTitle(R.string.menu_open_file_read_and_write); + togglePassword.setIcon(R.drawable.ic_read_write_white_24dp); + } + } + @Override public boolean onOptionsItemSelected(MenuItem item) { @@ -985,6 +1051,10 @@ public class PasswordActivity extends StylishActivity case android.R.id.home: finish(); break; + case R.id.menu_open_file_read_mode_key: + readOnly = !readOnly; + changeOpenFileReadIcon(item); + break; case R.id.menu_fingerprint_remove_key: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { deleteEntryKey(); diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java b/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java index 0f7595818..06ef0a21e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedSettingsFragment.java @@ -43,6 +43,7 @@ import android.widget.Toast; import com.kunzisoft.keepass.BuildConfig; import com.kunzisoft.keepass.R; +import com.kunzisoft.keepass.activities.ReadOnlyHelper; import com.kunzisoft.keepass.app.App; import com.kunzisoft.keepass.database.Database; import com.kunzisoft.keepass.dialogs.ProFeatureDialogFragment; @@ -72,6 +73,9 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat private static final int REQUEST_CODE_AUTOFILL = 5201; + private Database database; + private boolean databaseReadOnly; + private int count = 0; private Preference roundPref; @@ -79,14 +83,24 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat private Preference parallelismPref; public static NestedSettingsFragment newInstance(Screen key) { + return newInstance(key, ReadOnlyHelper.READ_ONLY_DEFAULT); + } + + public static NestedSettingsFragment newInstance(Screen key, boolean databaseReadOnly) { NestedSettingsFragment fragment = new NestedSettingsFragment(); // supply arguments to bundle. Bundle args = new Bundle(); args.putInt(TAG_KEY, key.ordinal()); + ReadOnlyHelper.putReadOnlyInBundle(args, databaseReadOnly); fragment.setArguments(args); return fragment; } + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + @Override public void onResume() { super.onResume(); @@ -110,6 +124,10 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat if (getArguments() != null) key = getArguments().getInt(TAG_KEY); + database = App.getDB(); + databaseReadOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, getArguments()); + databaseReadOnly = database.isReadOnly() || databaseReadOnly; + // Load the preferences from an XML resource switch (Screen.values()[key]) { case APPLICATION: @@ -306,23 +324,22 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat case DATABASE: setPreferencesFromResource(R.xml.database_preferences, rootKey); - Database db = App.getDB(); - if (db.getLoaded()) { + if (database.getLoaded()) { PreferenceCategory dbGeneralPrefCategory = (PreferenceCategory) findPreference(getString(R.string.database_general_key)); // Db name Preference dbNamePref = findPreference(getString(R.string.database_name_key)); - if ( db.containsName() ) { - dbNamePref.setSummary(db.getName()); + if ( database.containsName() ) { + dbNamePref.setSummary(database.getName()); } else { dbGeneralPrefCategory.removePreference(dbNamePref); } // Db description Preference dbDescriptionPref = findPreference(getString(R.string.database_description_key)); - if ( db.containsDescription() ) { - dbDescriptionPref.setSummary(db.getDescription()); + if ( database.containsDescription() ) { + dbDescriptionPref.setSummary(database.getDescription()); } else { dbGeneralPrefCategory.removePreference(dbDescriptionPref); } @@ -331,9 +348,9 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat SwitchPreference recycleBinPref = (SwitchPreference) findPreference(getString(R.string.recycle_bin_key)); // TODO Recycle dbGeneralPrefCategory.removePreference(recycleBinPref); // To delete - if (db.isRecycleBinAvailable()) { + if (database.isRecycleBinAvailable()) { - recycleBinPref.setChecked(db.isRecycleBinEnabled()); + recycleBinPref.setChecked(database.isRecycleBinEnabled()); recycleBinPref.setEnabled(false); } else { dbGeneralPrefCategory.removePreference(recycleBinPref); @@ -341,27 +358,27 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat // Version Preference dbVersionPref = findPreference(getString(R.string.database_version_key)); - dbVersionPref.setSummary(db.getVersion()); + dbVersionPref.setSummary(database.getVersion()); // Encryption Algorithm Preference algorithmPref = findPreference(getString(R.string.encryption_algorithm_key)); - algorithmPref.setSummary(db.getEncryptionAlgorithmName(getResources())); + algorithmPref.setSummary(database.getEncryptionAlgorithmName(getResources())); // Key derivation function Preference kdfPref = findPreference(getString(R.string.key_derivation_function_key)); - kdfPref.setSummary(db.getKeyDerivationName(getResources())); + kdfPref.setSummary(database.getKeyDerivationName(getResources())); // Round encryption roundPref = findPreference(getString(R.string.transform_rounds_key)); - roundPref.setSummary(db.getNumberKeyEncryptionRoundsAsString()); + roundPref.setSummary(database.getNumberKeyEncryptionRoundsAsString()); // Memory Usage memoryPref = findPreference(getString(R.string.memory_usage_key)); - memoryPref.setSummary(db.getMemoryUsageAsString()); + memoryPref.setSummary(database.getMemoryUsageAsString()); // Parallelism parallelismPref = findPreference(getString(R.string.parallelism_key)); - parallelismPref.setSummary(db.getParallelismAsString()); + parallelismPref.setSummary(database.getParallelismAsString()); } else { Log.e(getClass().getName(), "Database isn't ready"); @@ -480,18 +497,16 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat assert getFragmentManager() != null; - DialogFragment dialogFragment = null; + boolean otherDialogFragment = false; + DialogFragment dialogFragment = null; if (preference.getKey().equals(getString(R.string.database_name_key))) { dialogFragment = DatabaseNamePreferenceDialogFragmentCompat.newInstance(preference.getKey()); - } - else if (preference.getKey().equals(getString(R.string.database_description_key))) { + } else if (preference.getKey().equals(getString(R.string.database_description_key))) { dialogFragment = DatabaseDescriptionPreferenceDialogFragmentCompat.newInstance(preference.getKey()); - } - else if (preference.getKey().equals(getString(R.string.encryption_algorithm_key))) { + } else if (preference.getKey().equals(getString(R.string.encryption_algorithm_key))) { dialogFragment = DatabaseEncryptionAlgorithmPreferenceDialogFragmentCompat.newInstance(preference.getKey()); - } - else if (preference.getKey().equals(getString(R.string.key_derivation_function_key))) { + } else if (preference.getKey().equals(getString(R.string.key_derivation_function_key))) { DatabaseKeyDerivationPreferenceDialogFragmentCompat keyDerivationDialogFragment = DatabaseKeyDerivationPreferenceDialogFragmentCompat.newInstance(preference.getKey()); // Add other prefs to manage if (roundPref != null) @@ -501,24 +516,23 @@ public class NestedSettingsFragment extends PreferenceFragmentCompat if (parallelismPref != null) keyDerivationDialogFragment.setParallelismPreference(parallelismPref); dialogFragment = keyDerivationDialogFragment; - } - else if (preference.getKey().equals(getString(R.string.transform_rounds_key))) { + } else if (preference.getKey().equals(getString(R.string.transform_rounds_key))) { dialogFragment = RoundsPreferenceDialogFragmentCompat.newInstance(preference.getKey()); - } - else if (preference.getKey().equals(getString(R.string.memory_usage_key))) { + } else if (preference.getKey().equals(getString(R.string.memory_usage_key))) { dialogFragment = MemoryUsagePreferenceDialogFragmentCompat.newInstance(preference.getKey()); - } - else if (preference.getKey().equals(getString(R.string.parallelism_key))) { + } else if (preference.getKey().equals(getString(R.string.parallelism_key))) { dialogFragment = ParallelismPreferenceDialogFragmentCompat.newInstance(preference.getKey()); + } else { + otherDialogFragment = true; } - if (dialogFragment != null) { + if (dialogFragment != null && !databaseReadOnly) { dialogFragment.setTargetFragment(this, 0); dialogFragment.show(getFragmentManager(), null); } // Could not be handled here. Try with the super method. - else { + else if (otherDialogFragment) { super.onDisplayPreferenceDialog(preference); } } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.java b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.java index 932b2d1f8..904c648ae 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.java +++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.java @@ -158,6 +158,12 @@ public class PreferencesUtil { context.getResources().getBoolean(R.bool.allow_no_password_default)); } + public static boolean enableReadOnlyDatabase(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return prefs.getBoolean(context.getString(R.string.enable_read_only_key), + context.getResources().getBoolean(R.bool.enable_read_only_default)); + } + /** * All preference keys associated with education */ @@ -166,6 +172,7 @@ public class PreferencesUtil { R.string.education_select_db_key, R.string.education_open_link_db_key, R.string.education_unlock_key, + R.string.education_read_only_key, R.string.education_search_key, R.string.education_new_node_key, R.string.education_sort_key, @@ -245,6 +252,18 @@ public class PreferencesUtil { context.getResources().getBoolean(R.bool.education_unlock_default)); } + /** + * Determines whether the explanatory view of the database read-only has already been displayed. + * + * @param context The context to open the SharedPreferences + * @return boolean value of education_read_only_key key + */ + public static boolean isEducationReadOnlyPerformed(Context context) { + SharedPreferences prefs = getEducationSharedPreferences(context); + return prefs.getBoolean(context.getString(R.string.education_read_only_key), + context.getResources().getBoolean(R.bool.education_read_only_default)); + } + /** * Determines whether the explanatory view of search has already been displayed. * diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.java b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.java index ef61f9dbc..2b3db66ce 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.java +++ b/app/src/main/java/com/kunzisoft/keepass/settings/SettingsActivity.java @@ -28,6 +28,7 @@ import android.support.v7.widget.Toolbar; import android.view.MenuItem; import com.kunzisoft.keepass.R; +import com.kunzisoft.keepass.activities.ReadOnlyHelper; import com.kunzisoft.keepass.lock.LockingActivity; @@ -39,17 +40,20 @@ public class SettingsActivity extends LockingActivity implements MainPreferenceF private Toolbar toolbar; - public static void launch(Activity activity) { - Intent i = new Intent(activity, SettingsActivity.class); - activity.startActivity(i); + private boolean readOnly; + + public static void launch(Activity activity, boolean readOnly) { + Intent intent = new Intent(activity, SettingsActivity.class); + ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly); + activity.startActivity(intent); } - public static void launch(Activity activity, boolean checkLock) { + public static void launch(Activity activity, boolean readOnly, boolean checkLock) { // To avoid flickering when launch settings in a LockingActivity if (!checkLock) - launch(activity); + launch(activity, readOnly); else if (LockingActivity.checkTimeIsAllowedOrFinish(activity)) { - launch(activity); + launch(activity, readOnly); } } @@ -72,6 +76,8 @@ public class SettingsActivity extends LockingActivity implements MainPreferenceF assert getSupportActionBar() != null; getSupportActionBar().setDisplayHomeAsUpEnabled(true); + readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, getIntent()); + if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .add(R.id.fragment_container, retrieveMainFragment()) @@ -81,6 +87,12 @@ public class SettingsActivity extends LockingActivity implements MainPreferenceF backupManager = new BackupManager(this); } + @Override + public void onSaveInstanceState(Bundle outState) { + ReadOnlyHelper.onSaveInstanceState(outState, readOnly); + super.onSaveInstanceState(outState); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch ( item.getItemId() ) { @@ -114,7 +126,7 @@ public class SettingsActivity extends LockingActivity implements MainPreferenceF getSupportFragmentManager().beginTransaction() .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right) - .replace(R.id.fragment_container, NestedSettingsFragment.newInstance(key), TAG_NESTED) + .replace(R.id.fragment_container, NestedSettingsFragment.newInstance(key, readOnly), TAG_NESTED) .addToBackStack(TAG_NESTED) .commit(); diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.java b/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.java index eb372fd91..a5ad75db8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.java +++ b/app/src/main/java/com/kunzisoft/keepass/utils/MenuUtil.java @@ -32,6 +32,8 @@ import com.kunzisoft.keepass.activities.AboutActivity; import com.kunzisoft.keepass.settings.SettingsActivity; import com.kunzisoft.keepass.stylish.StylishActivity; +import static com.kunzisoft.keepass.activities.ReadOnlyHelper.READ_ONLY_DEFAULT; + public class MenuUtil { @@ -56,20 +58,20 @@ public class MenuUtil { } public static boolean onDefaultMenuOptionsItemSelected(StylishActivity activity, MenuItem item) { - return onDefaultMenuOptionsItemSelected(activity, item, false); + return onDefaultMenuOptionsItemSelected(activity, item, READ_ONLY_DEFAULT, false); } /* * @param checkLock Check the time lock before launch settings in LockingActivity */ - public static boolean onDefaultMenuOptionsItemSelected(StylishActivity activity, MenuItem item, boolean checkLock) { + public static boolean onDefaultMenuOptionsItemSelected(StylishActivity activity, MenuItem item, boolean readOnly, boolean checkLock) { switch (item.getItemId()) { case R.id.menu_contribute: return onContributionItemSelected(activity); case R.id.menu_app_settings: // To avoid flickering when launch settings in a LockingActivity - SettingsActivity.launch(activity, checkLock); + SettingsActivity.launch(activity, readOnly, checkLock); return true; case R.id.menu_about: diff --git a/app/src/main/java/com/kunzisoft/keepass/view/AddNodeButtonView.java b/app/src/main/java/com/kunzisoft/keepass/view/AddNodeButtonView.java index 7bcc65caf..b138b8da7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/AddNodeButtonView.java +++ b/app/src/main/java/com/kunzisoft/keepass/view/AddNodeButtonView.java @@ -167,6 +167,7 @@ public class AddNodeButtonView extends RelativeLayout { this.addEntryEnable = enable; if (enable && addEntryView != null && addEntryView.getVisibility() != VISIBLE) addEntryView.setVisibility(INVISIBLE); + disableViewIfNoAddAvailable(); } /** @@ -177,6 +178,19 @@ public class AddNodeButtonView extends RelativeLayout { this.addGroupEnable = enable; if (enable && addGroupView != null && addGroupView.getVisibility() != VISIBLE) addGroupView.setVisibility(INVISIBLE); + disableViewIfNoAddAvailable(); + } + + private void disableViewIfNoAddAvailable() { + if (!addEntryEnable || !addGroupEnable) { + setVisibility(GONE); + } else { + setVisibility(VISIBLE); + } + } + + public boolean isVisible() { + return getVisibility() == VISIBLE; } public void setAddGroupClickListener(OnClickListener onClickListener) { diff --git a/app/src/main/res/drawable/ic_read_only_white_24dp.xml b/app/src/main/res/drawable/ic_read_only_white_24dp.xml new file mode 100644 index 000000000..7f6522b98 --- /dev/null +++ b/app/src/main/res/drawable/ic_read_only_white_24dp.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_read_write_white_24dp.xml b/app/src/main/res/drawable/ic_read_write_white_24dp.xml new file mode 100644 index 000000000..db54875cd --- /dev/null +++ b/app/src/main/res/drawable/ic_read_write_white_24dp.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/open_file.xml b/app/src/main/res/menu/open_file.xml new file mode 100644 index 000000000..1ca2c2768 --- /dev/null +++ b/app/src/main/res/menu/open_file.xml @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index dd3f69706..dc0795774 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -88,6 +88,7 @@ education_select_db_key education_open_link_db_key education_unlock_key + education_read_only_key education_search_key education_new_node_key education_sort_key @@ -102,6 +103,7 @@ magic_keyboard_key magic_keyboard_preference_key allow_no_password_key + enable_read_only_key true true @@ -124,6 +126,7 @@ false false false + false false false false @@ -135,6 +138,7 @@ false false true + false 300000 60000 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d9491a06e..a64af391c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -134,6 +134,8 @@ Show pass Remove the fingerprint key Go to URL + Read only + Read and write Minus Never No search results @@ -280,6 +282,8 @@ Allow no password Enable the open button if no password identification is selected. + Read only + By default, open a read-only database. Education screens Highlight the elements to learn how the application works @@ -306,6 +310,8 @@ You want to register a basic non-supplied field, simply fill in a new one that you can also protect visually. Unlock your database Enter a password and/or a key file to unlock your database.\n\nRemember to save a copy of your file in a safe place after each modification. + Enable read-only + Change the opening mode for the session.\n\nIn read-only mode, you prevent unintended changes to the database.\n\nIn write mode, you can add, delete, or modify all the elements as you want. Copy a field Copy a field easily to paste it where you want\n\nYou can use several forms filling methods. Use the one you prefer! Lock the database diff --git a/app/src/main/res/xml/application_preferences.xml b/app/src/main/res/xml/application_preferences.xml index 0bc6cc08c..5c4d082ec 100644 --- a/app/src/main/res/xml/application_preferences.xml +++ b/app/src/main/res/xml/application_preferences.xml @@ -28,6 +28,11 @@ android:defaultValue="@bool/allow_no_password_default" android:title="@string/allow_no_password_title" android:key="@string/allow_no_password_key"/> + diff --git a/art/ic_read_only.svg b/art/ic_read_only.svg new file mode 100644 index 000000000..25fb8870f --- /dev/null +++ b/art/ic_read_only.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/art/ic_read_write.svg b/art/ic_read_write.svg new file mode 100644 index 000000000..627c1fc6b --- /dev/null +++ b/art/ic_read_write.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + +