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:
Harsh Shandilya
2021-04-21 18:10:19 +05:30
parent 6ff01f5e1e
commit a5b6dfc106
10 changed files with 53 additions and 75 deletions

View File

@@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file.
- Add support for manually providing TOTP parameters - Add support for manually providing TOTP parameters
- Parse extra content as individual fields - Parse extra content as individual fields
- Improve search result filtering logic - Improve search result filtering logic
- Allow pinning shortcuts directly to the launcher home screen
### Fixed ### Fixed

View File

@@ -4,7 +4,11 @@
*/ */
package dev.msfjarvis.aps.data.password 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.crypto.BasePgpActivity
import dev.msfjarvis.aps.ui.main.LaunchActivity
import java.io.File import java.io.File
data class PasswordItem( data class PasswordItem(
@@ -35,6 +39,16 @@ data class PasswordItem(
return 0 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 { companion object {
const val TYPE_CATEGORY = 'c' const val TYPE_CATEGORY = 'c'

View File

@@ -13,7 +13,6 @@ import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils
import android.view.WindowManager import android.view.WindowManager
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.annotation.StringRes import androidx.annotation.StringRes
@@ -55,11 +54,6 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
*/ */
val name: String by lazy(LazyThreadSafetyMode.NONE) { File(fullPath).nameWithoutExtension } 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 */ /** [SharedPreferences] instance used by subclasses to persist settings */
val settings: SharedPreferences by lazy(LazyThreadSafetyMode.NONE) { sharedPrefs } 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 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 * 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. * use this when they want to default to sane error handling.
@@ -256,8 +241,6 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
companion object { companion object {
private const val TAG = "APS/BasePgpActivity" 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 */ /** Gets the relative path to the repository */
fun getRelativePath(fullPath: String, repositoryPath: String): String = fun getRelativePath(fullPath: String, repositoryPath: String): String =

View File

@@ -9,7 +9,6 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.activity.result.IntentSenderRequest import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -78,11 +77,6 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
copyTextToClipboard(name) copyTextToClipboard(name)
true true
} }
passwordLastChanged.run {
runCatching { text = resources.getString(R.string.last_changed, lastChangedString) }.onFailure {
visibility = View.GONE
}
}
} }
} }

View File

@@ -24,6 +24,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.github.michaelbull.result.fold import com.github.michaelbull.result.fold
import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching import com.github.michaelbull.result.runCatching
import dagger.hilt.android.AndroidEntryPoint
import dev.msfjarvis.aps.R import dev.msfjarvis.aps.R
import dev.msfjarvis.aps.data.password.PasswordItem import dev.msfjarvis.aps.data.password.PasswordItem
import dev.msfjarvis.aps.data.repo.PasswordRepository 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.GitSettings
import dev.msfjarvis.aps.util.settings.PasswordSortOrder import dev.msfjarvis.aps.util.settings.PasswordSortOrder
import dev.msfjarvis.aps.util.settings.PreferenceKeys import dev.msfjarvis.aps.util.settings.PreferenceKeys
import dev.msfjarvis.aps.util.shortcuts.ShortcutHandler
import dev.msfjarvis.aps.util.viewmodel.SearchableRepositoryViewModel import dev.msfjarvis.aps.util.viewmodel.SearchableRepositoryViewModel
import java.io.File import java.io.File
import javax.inject.Inject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zhanghai.android.fastscroll.FastScrollerBuilder import me.zhanghai.android.fastscroll.FastScrollerBuilder
@AndroidEntryPoint
class PasswordFragment : Fragment(R.layout.password_recycler_view) { class PasswordFragment : Fragment(R.layout.password_recycler_view) {
@Inject lateinit var shortcutHandler: ShortcutHandler
private lateinit var recyclerAdapter: PasswordItemRecyclerAdapter private lateinit var recyclerAdapter: PasswordItemRecyclerAdapter
private lateinit var listener: OnFragmentInteractionListener private lateinit var listener: OnFragmentInteractionListener
private lateinit var settings: SharedPreferences 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, // Called each time the action mode is shown. Always called after onCreateActionMode,
// but // but may be called multiple times if the mode is invalidated.
// may be called multiple times if the mode is invalidated.
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
menu.findItem(R.id.menu_edit_password).isVisible = val selectedItems = recyclerAdapter.getSelectedItems()
recyclerAdapter.getSelectedItems().all { it.type == PasswordItem.TYPE_CATEGORY } 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 return true
} }
@@ -219,6 +225,11 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
mode.finish() mode.finish()
false false
} }
R.id.menu_pin_password -> {
val passwordItem = recyclerAdapter.getSelectedItems()[0]
shortcutHandler.addPinnedShortcut(passwordItem, passwordItem.createAuthEnabledIntent(requireContext()))
false
}
else -> false else -> false
} }
} }

View File

@@ -6,6 +6,7 @@ package dev.msfjarvis.aps.ui.passwords
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@@ -26,9 +27,7 @@ import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.d import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
import com.github.ajalt.timberkt.i import com.github.ajalt.timberkt.i
import com.github.ajalt.timberkt.w
import com.github.michaelbull.result.fold import com.github.michaelbull.result.fold
import com.github.michaelbull.result.getOr
import com.github.michaelbull.result.onFailure import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching import com.github.michaelbull.result.runCatching
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.dialogs.FolderCreationDialogFragment
import dev.msfjarvis.aps.ui.folderselect.SelectFolderActivity import dev.msfjarvis.aps.ui.folderselect.SelectFolderActivity
import dev.msfjarvis.aps.ui.git.base.BaseGitActivity 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.onboarding.activity.OnboardingActivity
import dev.msfjarvis.aps.ui.settings.DirectorySelectionActivity import dev.msfjarvis.aps.ui.settings.DirectorySelectionActivity
import dev.msfjarvis.aps.ui.settings.SettingsActivity import dev.msfjarvis.aps.ui.settings.SettingsActivity
@@ -69,7 +67,6 @@ import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.eclipse.jgit.api.Git
const val PASSWORD_FRAGMENT_TAG = "PasswordsList" const val PASSWORD_FRAGMENT_TAG = "PasswordsList"
@@ -403,37 +400,10 @@ class PasswordStore : BaseGitActivity() {
return fullPath.replace(repositoryPath, "").replace("/+".toRegex(), "/") 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) { fun decryptPassword(item: PasswordItem) {
val decryptIntent = Intent(this, DecryptActivity::class.java) val authDecryptIntent = item.createAuthEnabledIntent(this)
val authDecryptIntent = Intent(this, LaunchActivity::class.java) val decryptIntent =
for (intent in arrayOf(decryptIntent, authDecryptIntent)) { (authDecryptIntent.clone() as Intent).setComponent(ComponentName(this, DecryptActivity::class.java))
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
startActivity(decryptIntent) startActivity(decryptIntent)

View 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>

View File

@@ -38,19 +38,6 @@
app:layout_constraintTop_toBottomOf="@id/password_category" app:layout_constraintTop_toBottomOf="@id/password_category"
tools:text="PASSWORD FILE NAME HERE" /> 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 <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/divider" android:id="@+id/divider"
@@ -59,7 +46,7 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:src="@drawable/divider" android:src="@drawable/divider"
app:layout_constraintTop_toBottomOf="@id/password_last_changed" app:layout_constraintTop_toBottomOf="@id/password_file"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView

View File

@@ -25,4 +25,10 @@
android:icon="@drawable/ic_edit_24dp" android:icon="@drawable/ic_edit_24dp"
android:title="@string/edit" android:title="@string/edit"
app:showAsAction="ifRoom" /> 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> </menu>

View File

@@ -405,5 +405,6 @@
<string name="otp_import_manual_hint_secret">Secret</string> <string name="otp_import_manual_hint_secret">Secret</string>
<string name="otp_import_manual_hint_account">Account</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="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> </resources>