Support creating pinned shortcuts directly (#1393)
* CHANGELOG: update for pinning support * PasswordFragment: support pinning * PasswordStore: use `PasswordItem#createAuthEnabledIntent` * PasswordItem: add `createAuthEnabledIntent` API * DecryptActivity: remove last changed time Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
parent
6ff01f5e1e
commit
a5b6dfc106
10 changed files with 53 additions and 75 deletions
|
@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file.
|
|||
- Add support for manually providing TOTP parameters
|
||||
- Parse extra content as individual fields
|
||||
- Improve search result filtering logic
|
||||
- Allow pinning shortcuts directly to the launcher home screen
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
*/
|
||||
package dev.msfjarvis.aps.data.password
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import dev.msfjarvis.aps.data.repo.PasswordRepository
|
||||
import dev.msfjarvis.aps.ui.crypto.BasePgpActivity
|
||||
import dev.msfjarvis.aps.ui.main.LaunchActivity
|
||||
import java.io.File
|
||||
|
||||
data class PasswordItem(
|
||||
|
@ -35,6 +39,16 @@ data class PasswordItem(
|
|||
return 0
|
||||
}
|
||||
|
||||
/** Creates an [Intent] to launch this [PasswordItem] through the authentication process. */
|
||||
fun createAuthEnabledIntent(context: Context): Intent {
|
||||
val intent = Intent(context, LaunchActivity::class.java)
|
||||
intent.putExtra("NAME", toString())
|
||||
intent.putExtra("FILE_PATH", file.absolutePath)
|
||||
intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory().absolutePath)
|
||||
intent.action = LaunchActivity.ACTION_DECRYPT_PASS
|
||||
return intent
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TYPE_CATEGORY = 'c'
|
||||
|
|
|
@ -13,7 +13,6 @@ import android.content.SharedPreferences
|
|||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.StringRes
|
||||
|
@ -55,11 +54,6 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
|
|||
*/
|
||||
val name: String by lazy(LazyThreadSafetyMode.NONE) { File(fullPath).nameWithoutExtension }
|
||||
|
||||
/** Get the timestamp for when this file was last modified. */
|
||||
val lastChangedString: CharSequence by lazy(LazyThreadSafetyMode.NONE) {
|
||||
getLastChangedString(intent.getLongExtra("LAST_CHANGED_TIMESTAMP", -1L))
|
||||
}
|
||||
|
||||
/** [SharedPreferences] instance used by subclasses to persist settings */
|
||||
val settings: SharedPreferences by lazy(LazyThreadSafetyMode.NONE) { sharedPrefs }
|
||||
|
||||
|
@ -177,15 +171,6 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
|
|||
return result.getParcelableExtra<PendingIntent>(OpenPgpApi.RESULT_INTENT)!!.intentSender
|
||||
}
|
||||
|
||||
/** Gets a relative string describing when this shape was last changed (e.g. "one hour ago") */
|
||||
private fun getLastChangedString(timeStamp: Long): CharSequence {
|
||||
if (timeStamp < 0) {
|
||||
throw RuntimeException()
|
||||
}
|
||||
|
||||
return DateUtils.getRelativeTimeSpanString(this, timeStamp, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Base handling of OpenKeychain errors based on the error contained in [result]. Subclasses can
|
||||
* use this when they want to default to sane error handling.
|
||||
|
@ -256,8 +241,6 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
|
|||
companion object {
|
||||
|
||||
private const val TAG = "APS/BasePgpActivity"
|
||||
const val KEY_PWGEN_TYPE_CLASSIC = "classic"
|
||||
const val KEY_PWGEN_TYPE_XKPASSWD = "xkpasswd"
|
||||
|
||||
/** Gets the relative path to the repository */
|
||||
fun getRelativePath(fullPath: String, repositoryPath: String): String =
|
||||
|
|
|
@ -9,7 +9,6 @@ import android.content.Intent
|
|||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.result.IntentSenderRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -78,11 +77,6 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
|
|||
copyTextToClipboard(name)
|
||||
true
|
||||
}
|
||||
passwordLastChanged.run {
|
||||
runCatching { text = resources.getString(R.string.last_changed, lastChangedString) }.onFailure {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import com.github.michaelbull.result.fold
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.msfjarvis.aps.R
|
||||
import dev.msfjarvis.aps.data.password.PasswordItem
|
||||
import dev.msfjarvis.aps.data.repo.PasswordRepository
|
||||
|
@ -42,13 +43,17 @@ import dev.msfjarvis.aps.util.settings.AuthMode
|
|||
import dev.msfjarvis.aps.util.settings.GitSettings
|
||||
import dev.msfjarvis.aps.util.settings.PasswordSortOrder
|
||||
import dev.msfjarvis.aps.util.settings.PreferenceKeys
|
||||
import dev.msfjarvis.aps.util.shortcuts.ShortcutHandler
|
||||
import dev.msfjarvis.aps.util.viewmodel.SearchableRepositoryViewModel
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
import me.zhanghai.android.fastscroll.FastScrollerBuilder
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
||||
|
||||
@Inject lateinit var shortcutHandler: ShortcutHandler
|
||||
private lateinit var recyclerAdapter: PasswordItemRecyclerAdapter
|
||||
private lateinit var listener: OnFragmentInteractionListener
|
||||
private lateinit var settings: SharedPreferences
|
||||
|
@ -193,11 +198,12 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
}
|
||||
|
||||
// Called each time the action mode is shown. Always called after onCreateActionMode,
|
||||
// but
|
||||
// may be called multiple times if the mode is invalidated.
|
||||
// but may be called multiple times if the mode is invalidated.
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
menu.findItem(R.id.menu_edit_password).isVisible =
|
||||
recyclerAdapter.getSelectedItems().all { it.type == PasswordItem.TYPE_CATEGORY }
|
||||
val selectedItems = recyclerAdapter.getSelectedItems()
|
||||
menu.findItem(R.id.menu_edit_password).isVisible = selectedItems.all { it.type == PasswordItem.TYPE_CATEGORY }
|
||||
menu.findItem(R.id.menu_pin_password).isVisible =
|
||||
selectedItems.size == 1 && selectedItems[0].type == PasswordItem.TYPE_PASSWORD
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -219,6 +225,11 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
|||
mode.finish()
|
||||
false
|
||||
}
|
||||
R.id.menu_pin_password -> {
|
||||
val passwordItem = recyclerAdapter.getSelectedItems()[0]
|
||||
shortcutHandler.addPinnedShortcut(passwordItem, passwordItem.createAuthEnabledIntent(requireContext()))
|
||||
false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ package dev.msfjarvis.aps.ui.passwords
|
|||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
|
@ -26,9 +27,7 @@ import androidx.lifecycle.lifecycleScope
|
|||
import com.github.ajalt.timberkt.d
|
||||
import com.github.ajalt.timberkt.e
|
||||
import com.github.ajalt.timberkt.i
|
||||
import com.github.ajalt.timberkt.w
|
||||
import com.github.michaelbull.result.fold
|
||||
import com.github.michaelbull.result.getOr
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
@ -44,7 +43,6 @@ import dev.msfjarvis.aps.ui.dialogs.BasicBottomSheet
|
|||
import dev.msfjarvis.aps.ui.dialogs.FolderCreationDialogFragment
|
||||
import dev.msfjarvis.aps.ui.folderselect.SelectFolderActivity
|
||||
import dev.msfjarvis.aps.ui.git.base.BaseGitActivity
|
||||
import dev.msfjarvis.aps.ui.main.LaunchActivity
|
||||
import dev.msfjarvis.aps.ui.onboarding.activity.OnboardingActivity
|
||||
import dev.msfjarvis.aps.ui.settings.DirectorySelectionActivity
|
||||
import dev.msfjarvis.aps.ui.settings.SettingsActivity
|
||||
|
@ -69,7 +67,6 @@ import javax.inject.Inject
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.eclipse.jgit.api.Git
|
||||
|
||||
const val PASSWORD_FRAGMENT_TAG = "PasswordsList"
|
||||
|
||||
|
@ -403,37 +400,10 @@ class PasswordStore : BaseGitActivity() {
|
|||
return fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/")
|
||||
}
|
||||
|
||||
private fun getLastChangedTimestamp(fullPath: String): Long {
|
||||
val repoPath = PasswordRepository.getRepositoryDirectory()
|
||||
val repository = PasswordRepository.getRepository(repoPath)
|
||||
if (repository == null) {
|
||||
d { "getLastChangedTimestamp: No git repository" }
|
||||
return File(fullPath).lastModified()
|
||||
}
|
||||
val git = Git(repository)
|
||||
val relativePath = getRelativePath(fullPath, repoPath.absolutePath).substring(1) // Removes leading '/'
|
||||
return runCatching {
|
||||
val iterator = git.log().addPath(relativePath).call().iterator()
|
||||
if (!iterator.hasNext()) {
|
||||
w { "getLastChangedTimestamp: No commits for file: $relativePath" }
|
||||
return -1
|
||||
}
|
||||
iterator.next().commitTime.toLong() * 1000
|
||||
}
|
||||
.getOr(-1)
|
||||
}
|
||||
|
||||
fun decryptPassword(item: PasswordItem) {
|
||||
val decryptIntent = Intent(this, DecryptActivity::class.java)
|
||||
val authDecryptIntent = Intent(this, LaunchActivity::class.java)
|
||||
for (intent in arrayOf(decryptIntent, authDecryptIntent)) {
|
||||
intent.putExtra("NAME", item.toString())
|
||||
intent.putExtra("FILE_PATH", item.file.absolutePath)
|
||||
intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory().absolutePath)
|
||||
intent.putExtra("LAST_CHANGED_TIMESTAMP", getLastChangedTimestamp(item.file.absolutePath))
|
||||
}
|
||||
// Needs an action to be a shortcut intent
|
||||
authDecryptIntent.action = LaunchActivity.ACTION_DECRYPT_PASS
|
||||
val authDecryptIntent = item.createAuthEnabledIntent(this)
|
||||
val decryptIntent =
|
||||
(authDecryptIntent.clone() as Intent).setComponent(ComponentName(this, DecryptActivity::class.java))
|
||||
|
||||
startActivity(decryptIntent)
|
||||
|
||||
|
|
11
app/src/main/res/drawable/ic_push_pin_24dp.xml
Normal file
11
app/src/main/res/drawable/ic_push_pin_24dp.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16,9V4l1,0c0.55,0 1,-0.45 1,-1v0c0,-0.55 -0.45,-1 -1,-1H7C6.45,2 6,2.45 6,3v0c0,0.55 0.45,1 1,1l1,0v5c0,1.66 -1.34,3 -3,3h0v2h5.97v7l1,1l1,-1v-7H19v-2h0C17.34,12 16,10.66 16,9z"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -38,19 +38,6 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/password_category"
|
||||
tools:text="PASSWORD FILE NAME HERE" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/password_last_changed"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="16dp"
|
||||
android:textColor="?android:attr/textColor"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/password_file"
|
||||
tools:text="LAST CHANGED HERE" />
|
||||
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/divider"
|
||||
|
@ -59,7 +46,7 @@
|
|||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:src="@drawable/divider"
|
||||
app:layout_constraintTop_toBottomOf="@id/password_last_changed"
|
||||
app:layout_constraintTop_toBottomOf="@id/password_file"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
|
|
|
@ -25,4 +25,10 @@
|
|||
android:icon="@drawable/ic_edit_24dp"
|
||||
android:title="@string/edit"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_pin_password"
|
||||
android:icon="@drawable/ic_push_pin_24dp"
|
||||
android:title="@string/place_shortcut_on_home_screen"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
|
|
|
@ -405,5 +405,6 @@
|
|||
<string name="otp_import_manual_hint_secret">Secret</string>
|
||||
<string name="otp_import_manual_hint_account">Account</string>
|
||||
<string name="gpg_key_select_mandatory">Selecting a GPG key is necessary to proceed</string>
|
||||
<string name="place_shortcut_on_home_screen">Place shortcut on home screen</string>
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue