Files
Android/app/src/main/java/protect/card_locker/MainActivity.kt
T

946 lines
38 KiB
Kotlin

package protect.card_locker
import android.app.SearchManager
import android.content.DialogInterface
import android.content.Intent
import android.database.CursorIndexOutOfBoundsException
import android.database.sqlite.SQLiteDatabase
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import protect.card_locker.DBHelper.LoyaltyCardOrder
import protect.card_locker.DBHelper.LoyaltyCardOrderDirection
import protect.card_locker.LoyaltyCardCursorAdapter.CardAdapterListener
import protect.card_locker.cardview.LoyaltyCardViewActivity
import protect.card_locker.databinding.ContentMainBinding
import protect.card_locker.databinding.MainActivityBinding
import protect.card_locker.databinding.SortingOptionBinding
import protect.card_locker.preferences.Settings
import protect.card_locker.preferences.SettingsActivity
import java.io.UnsupportedEncodingException
import java.util.concurrent.atomic.AtomicInteger
import androidx.core.content.edit
class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
private lateinit var binding: MainActivityBinding
private lateinit var contentMainBinding: ContentMainBinding
private lateinit var mDatabase: SQLiteDatabase
private lateinit var mAdapter: LoyaltyCardCursorAdapter
private var mCurrentActionMode: ActionMode? = null
private var mSearchView: SearchView? = null
private var mLoyaltyCardCount = 0
@JvmField
var mFilter: String = ""
private var currentQuery = ""
private var finalQuery = ""
private var mGroup: Any? = null
private var mOrder: LoyaltyCardOrder = LoyaltyCardOrder.Alpha
private var mOrderDirection: LoyaltyCardOrderDirection = LoyaltyCardOrderDirection.Ascending
private var selectedTab: Int = 0
private lateinit var groupsTabLayout: TabLayout
private lateinit var mUpdateLoyaltyCardListRunnable: Runnable
private lateinit var mBarcodeScannerLauncher: ActivityResultLauncher<Intent>
private lateinit var mSettingsLauncher: ActivityResultLauncher<Intent>
private val mCurrentActionModeCallback: ActionMode.Callback = object : ActionMode.Callback {
override fun onCreateActionMode(inputMode: ActionMode, inputMenu: Menu?): Boolean {
inputMode.menuInflater.inflate(R.menu.card_longclick_menu, inputMenu)
return true
}
override fun onPrepareActionMode(inputMode: ActionMode?, inputMenu: Menu?): Boolean {
return false
}
override fun onActionItemClicked(inputMode: ActionMode, inputItem: MenuItem): Boolean {
when (inputItem.itemId) {
R.id.action_share -> {
try {
ImportURIHelper(this@MainActivity).startShareIntent(mAdapter.getSelectedItems())
} catch (e: UnsupportedEncodingException) {
Toast.makeText(
this@MainActivity,
R.string.failedGeneratingShareURL,
Toast.LENGTH_LONG
).show()
e.printStackTrace()
}
inputMode.finish()
return true
}
R.id.action_edit -> {
require(mAdapter.selectedItemCount == 1) { "Cannot edit more than 1 card at a time" }
startActivity(
Intent(applicationContext, LoyaltyCardEditActivity::class.java).apply {
putExtras(Bundle().apply {
putInt(
LoyaltyCardEditActivity.BUNDLE_ID,
mAdapter.getSelectedItems()[0].id
)
putBoolean(LoyaltyCardEditActivity.BUNDLE_UPDATE, true)
})
}
)
inputMode.finish()
return true
}
R.id.action_duplicate -> {
require(mAdapter.selectedItemCount == 1) { "Cannot duplicate more than 1 card at a time" }
startActivity(
Intent(applicationContext, LoyaltyCardEditActivity::class.java).apply {
putExtras(Bundle().apply {
putInt(
LoyaltyCardEditActivity.BUNDLE_ID,
mAdapter.getSelectedItems()[0].id
)
putBoolean(LoyaltyCardEditActivity.BUNDLE_DUPLICATE_ID, true)
})
}
)
inputMode.finish()
return true
}
R.id.action_delete -> {
MaterialAlertDialogBuilder(this@MainActivity).apply {
// The following may seem weird, but it is necessary to give translators enough flexibility.
// For example, in Russian, Android's plural quantity "one" actually refers to "any number ending on 1 but not ending in 11".
// So while in English the extra non-plural form seems unnecessary duplication, it is necessary to give translators enough flexibility.
// In here, we use the plain string when meaning exactly 1, and otherwise use the plural forms
if (mAdapter.selectedItemCount == 1) {
setTitle(R.string.deleteTitle)
setMessage(R.string.deleteConfirmation)
} else {
setTitle(
getResources().getQuantityString(
R.plurals.deleteCardsTitle,
mAdapter.selectedItemCount,
mAdapter.selectedItemCount
)
)
setMessage(
getResources().getQuantityString(
R.plurals.deleteCardsConfirmation,
mAdapter.selectedItemCount,
mAdapter.selectedItemCount
)
)
}
setPositiveButton(
R.string.confirm
) { dialog, _ ->
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Deleting card: " + loyaltyCard.id)
DBHelper.deleteLoyaltyCard(mDatabase, this@MainActivity, loyaltyCard.id)
}
val tab = groupsTabLayout.getTabAt(selectedTab)
mGroup = tab?.tag
updateLoyaltyCardList(true)
dialog.dismiss()
}
setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
}.create().show()
return true
}
R.id.action_archive -> {
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Archiving card: " + loyaltyCard.id)
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 1)
updateLoyaltyCardList(false)
inputMode.finish()
invalidateOptionsMenu()
}
return true
}
R.id.action_unarchive -> {
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Unarchiving card: " + loyaltyCard.id)
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 0)
updateLoyaltyCardList(false)
inputMode.finish()
invalidateOptionsMenu()
}
return true
}
R.id.action_star -> {
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Starring card: " + loyaltyCard.id)
DBHelper.updateLoyaltyCardStarStatus(mDatabase, loyaltyCard.id, 1)
updateLoyaltyCardList(false)
inputMode.finish()
}
return true
}
R.id.action_unstar -> {
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Unstarring card: " + loyaltyCard.id)
DBHelper.updateLoyaltyCardStarStatus(mDatabase, loyaltyCard.id, 0)
updateLoyaltyCardList(false)
inputMode.finish()
}
return true
}
}
return false
}
override fun onDestroyActionMode(inputMode: ActionMode?) {
mAdapter.clearSelections()
mCurrentActionMode = null
}
}
override fun onCreate(inputSavedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(inputSavedInstanceState)
// Delete old cache files
// These could be temporary images for the cropper, temporary images in LoyaltyCard toBundle/writeParcel/ etc.
Thread {
val twentyFourHoursAgo = System.currentTimeMillis() - (1000 * 60 * 60 * 24)
val tempFiles = cacheDir.listFiles()
if (tempFiles == null) {
Log.e(
TAG,
"getCacheDir().listFiles() somehow returned null, this should never happen... Skipping cache cleanup..."
)
return@Thread
}
for (file in tempFiles) {
if (file.lastModified() < twentyFourHoursAgo) {
if (!file.delete()) {
Log.w(TAG, "Failed to delete cache file " + file.path)
}
}
}
}.start()
// We should extract the share intent after we called the super.onCreate as it may need to spawn a dialog window and the app needs to be initialized to not crash
extractIntentFields(intent)
binding = MainActivityBinding.inflate(layoutInflater)
setContentView(binding.getRoot())
Utils.applyWindowInsets(binding.getRoot())
setSupportActionBar(binding.toolbar)
groupsTabLayout = binding.groups
contentMainBinding = ContentMainBinding.bind(binding.include.getRoot())
mDatabase = DBHelper(this).writableDatabase
mUpdateLoyaltyCardListRunnable = Runnable {
updateLoyaltyCardList(false)
}
groupsTabLayout.addOnTabSelectedListener(object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
selectedTab = tab.position
Log.d("onTabSelected", "Tab Position " + tab.position)
mGroup = tab.tag
updateLoyaltyCardList(false)
// Store active tab in Shared Preference to restore next app launch
applicationContext.getSharedPreferences(
getString(R.string.sharedpreference_active_tab),
MODE_PRIVATE
).edit {
putInt(
getString(R.string.sharedpreference_active_tab),
tab.position
)
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
mAdapter = LoyaltyCardCursorAdapter(this, null, this, mUpdateLoyaltyCardListRunnable)
contentMainBinding.list.setAdapter(mAdapter)
registerForContextMenu(contentMainBinding.list)
mBarcodeScannerLauncher = registerForActivityResult(
StartActivityForResult(),
ActivityResultCallback registerForActivityResult@{ result: ActivityResult? ->
// Exit early if the user cancelled the scan (pressed back/home)
if (result == null || result.resultCode != RESULT_OK) {
return@registerForActivityResult
}
startActivity(
Intent(applicationContext, LoyaltyCardEditActivity::class.java).apply {
putExtras(result.data!!.extras!!)
}
)
})
mSettingsLauncher = registerForActivityResult(
StartActivityForResult()
) { result: ActivityResult? ->
if (result?.resultCode == RESULT_OK) {
val intent = result.data
if (intent != null && intent.getBooleanExtra(RESTART_ACTIVITY_INTENT, false)) {
recreate()
}
}
}
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (mSearchView != null && !mSearchView!!.isIconified) {
mSearchView!!.isIconified = true
} else {
finish()
}
}
})
}
override fun onResume() {
super.onResume()
if (mCurrentActionMode != null) {
mAdapter.clearSelections()
mCurrentActionMode!!.finish()
}
if (mSearchView != null && !mSearchView!!.isIconified) {
mFilter = mSearchView!!.query.toString()
}
// Start of active tab logic
updateTabGroups(groupsTabLayout)
// Restore selected tab from Shared Preference
selectedTab = applicationContext.getSharedPreferences(
getString(R.string.sharedpreference_active_tab),
MODE_PRIVATE
).getInt(getString(R.string.sharedpreference_active_tab), 0)
// Restore sort preferences from Shared Preferences
mOrder = Utils.getLoyaltyCardOrder(this)
mOrderDirection = Utils.getLoyaltyCardOrderDirection(this)
mGroup = null
if (groupsTabLayout.tabCount != 0) {
var tab = groupsTabLayout.getTabAt(selectedTab)
if (tab == null) {
tab = groupsTabLayout.getTabAt(0)
}
groupsTabLayout.selectTab(tab)
checkNotNull(tab)
mGroup = tab.tag
} else {
scaleScreen()
}
updateLoyaltyCardList(true)
// End of active tab logic
binding.fabAdd.setOnClickListener {
mBarcodeScannerLauncher.launch(
Intent(applicationContext, ScanActivity::class.java).apply {
putExtras(Bundle().apply {
if (selectedTab != 0) {
putString(
LoyaltyCardEditActivity.BUNDLE_ADDGROUP,
groupsTabLayout.getTabAt(selectedTab)!!.text.toString()
)
}
})
}
)
}
binding.fabAdd.bringToFront()
// Apply column count setting to card list
val layoutManager = contentMainBinding.list.layoutManager as GridLayoutManager?
if (layoutManager != null) {
val settings = Settings(this)
layoutManager.setSpanCount(settings.getPreferredColumnCount())
}
}
private fun displayCardSetupOptions(menu: Menu, shouldShow: Boolean) {
for (id in intArrayOf(R.id.action_search, R.id.action_display_options, R.id.action_sort)) {
menu.findItem(id).isVisible = shouldShow
}
}
private fun updateLoyaltyCardCount() {
mLoyaltyCardCount = DBHelper.getLoyaltyCardCount(mDatabase)
}
private fun updateLoyaltyCardList(updateCount: Boolean) {
var group: Group? = null
if (mGroup != null) {
group = mGroup as Group
}
mAdapter.swapCursor(
DBHelper.getLoyaltyCardCursor(
mDatabase,
mFilter,
group,
mOrder,
mOrderDirection,
if (mAdapter.showingArchivedCards()) DBHelper.LoyaltyCardArchiveFilter.All else DBHelper.LoyaltyCardArchiveFilter.Unarchived
)
)
if (updateCount) {
updateLoyaltyCardCount()
// Update menu icons if necessary
invalidateOptionsMenu()
}
if (mLoyaltyCardCount > 0) {
// We want the cardList to be visible regardless of the filtered match count
// to ensure that the noMatchingCardsText doesn't end up being shown below
// the keyboard
contentMainBinding.helpSection.visibility = View.GONE
contentMainBinding.noGroupCardsText.visibility = View.GONE
if (mAdapter.itemCount > 0) {
contentMainBinding.list.visibility = View.VISIBLE
contentMainBinding.noMatchingCardsText.visibility = View.GONE
} else {
contentMainBinding.list.visibility = View.GONE
if (!mFilter.isEmpty()) {
// Actual Empty Search Result
contentMainBinding.noMatchingCardsText.visibility = View.VISIBLE
contentMainBinding.noGroupCardsText.visibility = View.GONE
} else {
// Group Tab with no Group Cards
contentMainBinding.noMatchingCardsText.visibility = View.GONE
contentMainBinding.noGroupCardsText.visibility = View.VISIBLE
}
}
} else {
contentMainBinding.list.visibility = View.GONE
contentMainBinding.helpSection.visibility = View.VISIBLE
contentMainBinding.noMatchingCardsText.visibility = View.GONE
contentMainBinding.noGroupCardsText.visibility = View.GONE
}
if (mCurrentActionMode != null) {
mCurrentActionMode!!.finish()
}
ListWidget().updateAll(mAdapter.mContext)
ShortcutHelper.updateShortcuts(mAdapter.mContext)
}
private fun processParseResultList(
parseResultList: MutableList<ParseResult?>,
group: String?,
closeAppOnNoBarcode: Boolean
) {
require(!parseResultList.isEmpty()) { "parseResultList may not be empty" }
Utils.makeUserChooseParseResultFromList(
this@MainActivity,
parseResultList,
object : ParseResultListDisambiguatorCallback {
override fun onUserChoseParseResult(parseResult: ParseResult) {
val intent =
Intent(applicationContext, LoyaltyCardEditActivity::class.java)
val bundle = parseResult.toLoyaltyCardBundle(this@MainActivity)
if (group != null) {
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, group)
}
intent.putExtras(bundle)
startActivity(intent)
}
override fun onUserDismissedSelector() {
if (closeAppOnNoBarcode) {
finish()
}
}
})
}
private fun onSharedIntent(intent: Intent) {
val receivedAction = intent.action
val receivedType = intent.type
if (receivedAction == null || receivedType == null) {
return
}
val parseResultList: MutableList<ParseResult?>?
// Check for shared text
if (receivedAction == Intent.ACTION_SEND && receivedType == "text/plain") {
val loyaltyCard = LoyaltyCard()
loyaltyCard.setCardId(intent.getStringExtra(Intent.EXTRA_TEXT)!!)
parseResultList = mutableListOf(ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard))
} else {
// Parse whatever file was sent, regardless of opening or sharing
val data: Uri? = when (receivedAction) {
Intent.ACTION_VIEW -> {
intent.data
}
Intent.ACTION_SEND -> {
intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
else -> {
Log.e(TAG, "Wrong action type to parse intent")
return
}
}
if (receivedType.startsWith("image/")) {
parseResultList = Utils.retrieveBarcodesFromImage(this, data)
} else if (receivedType == "application/pdf") {
parseResultList = Utils.retrieveBarcodesFromPdf(this, data)
} else if (mutableListOf<String?>(
"application/vnd.apple.pkpass",
"application/vnd-com.apple.pkpass"
).contains(receivedType)
) {
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data)
} else if (receivedType == "application/vnd.espass-espass") {
// FIXME: espass is not pkpass
// However, several users stated in https://github.com/CatimaLoyalty/Android/issues/2197 that the formats are extremely similar to the point they could rename an .espass file to .pkpass and have it imported
// So it makes sense to "unofficially" treat it as a PKPASS for now, even though not completely correct
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data)
} else if (receivedType == "application/vnd.apple.pkpasses") {
parseResultList = Utils.retrieveBarcodesFromPkPasses(this, data)
} else {
Log.e(TAG, "Wrong mime-type")
return
}
}
// Give up if we should parse but there is nothing to parse
if (parseResultList == null || parseResultList.isEmpty()) {
finish()
return
}
processParseResultList(parseResultList, null, true)
}
private fun extractIntentFields(intent: Intent) {
onSharedIntent(intent)
}
fun updateTabGroups(groupsTabLayout: TabLayout) {
val newGroups = DBHelper.getGroups(mDatabase)
if (newGroups.isEmpty()) {
groupsTabLayout.removeAllTabs()
groupsTabLayout.visibility = View.GONE
return
}
groupsTabLayout.removeAllTabs()
groupsTabLayout.addTab(
groupsTabLayout.newTab().apply {
setText(R.string.all)
tag = null
},
false
)
for (group in newGroups) {
groupsTabLayout.addTab(
groupsTabLayout.newTab().apply {
text = group._id
tag = group
},
false
)
}
groupsTabLayout.visibility = View.VISIBLE
}
// Saving currentQuery to finalQuery for user, this will be used to restore search history, happens when user clicks a card from list
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
finalQuery = currentQuery
// Putting the query also into outState for later use in onRestoreInstanceState when rotating screen
if (mSearchView != null) {
outState.putString(STATE_SEARCH_QUERY, finalQuery)
}
}
// Restoring instance state when rotation of screen happens with the goal to restore search query for user
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
finalQuery = savedInstanceState.getString(STATE_SEARCH_QUERY, "")
}
override fun onCreateOptionsMenu(inputMenu: Menu): Boolean {
menuInflater.inflate(R.menu.main_menu, inputMenu)
displayCardSetupOptions(inputMenu, mLoyaltyCardCount > 0)
val searchManager = getSystemService(SEARCH_SERVICE) as SearchManager?
if (searchManager != null) {
val searchMenuItem = inputMenu.findItem(R.id.action_search)
mSearchView = searchMenuItem.actionView as SearchView?
mSearchView!!.setSearchableInfo(searchManager.getSearchableInfo(componentName))
mSearchView!!.setSubmitButtonEnabled(false)
mSearchView!!.setOnCloseListener {
invalidateOptionsMenu()
false
}
/*
* On Android 13 and later, pressing Back while the search view is open hides the keyboard
* and collapses the search view at the same time.
* This brings back the old behavior on Android 12 and lower: pressing Back once
* hides the keyboard, press again while keyboard is hidden to collapse the search view.
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
searchMenuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
if (mSearchView!!.hasFocus()) {
mSearchView!!.clearFocus()
return false
}
currentQuery = ""
mFilter = ""
updateLoyaltyCardList(false)
return true
}
})
}
mSearchView!!.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String): Boolean {
mFilter = newText
// New logic to ensure search history after coming back from picked card - user will see the last search query
if (newText.isEmpty()) {
if (!finalQuery.isEmpty()) {
// Setting the query text for user after coming back from picked card from finalQuery
mSearchView!!.setQuery(finalQuery, false)
} else if (!currentQuery.isEmpty()) {
// Else if is needed in case user deletes search - expected behaviour is to show all cards
currentQuery = ""
mSearchView!!.setQuery(currentQuery, false)
}
} else {
// Setting search query each time user changes the text in search to temporary variable to be used later in finalQuery String which will be used to restore search history
currentQuery = newText
}
val currentTab =
groupsTabLayout.getTabAt(groupsTabLayout.selectedTabPosition)
mGroup = currentTab?.tag
updateLoyaltyCardList(false)
return true
}
})
// Check if we came from a picked card back to search, in that case we want to show the search view with previous search query
if (!finalQuery.isEmpty()) {
// Expand the search view to show the query
searchMenuItem.expandActionView()
// Setting the query text to empty String due to behaviour of onQueryTextChange after coming back from picked card - onQueryTextChange is called automatically without users interaction
finalQuery = ""
mSearchView!!.setQuery(currentQuery, false)
}
}
return super.onCreateOptionsMenu(inputMenu)
}
override fun onOptionsItemSelected(inputItem: MenuItem): Boolean {
when (inputItem.itemId) {
android.R.id.home -> {
onBackPressedDispatcher.onBackPressed()
}
R.id.action_display_options -> {
mAdapter.showDisplayOptionsDialog()
invalidateOptionsMenu()
return true
}
R.id.action_sort -> {
val currentIndex = AtomicInteger()
val loyaltyCardOrders = listOf<LoyaltyCardOrder?>(*LoyaltyCardOrder.entries.toTypedArray())
for (i in loyaltyCardOrders.indices) {
if (mOrder == loyaltyCardOrders[i]) {
currentIndex.set(i)
break
}
}
MaterialAlertDialogBuilder(this@MainActivity).apply {
setTitle(R.string.sort_by)
val sortingOptionBinding = SortingOptionBinding.inflate(LayoutInflater.from(this@MainActivity), null, false)
val customLayout: View = sortingOptionBinding.getRoot()
setView(customLayout)
val showReversed = sortingOptionBinding.checkBoxReverse
showReversed.isChecked = mOrderDirection == LoyaltyCardOrderDirection.Descending
setSingleChoiceItems(
R.array.sort_types_array,
currentIndex.get()
) { _: DialogInterface?, which: Int ->
currentIndex.set(which)
}
setPositiveButton(
R.string.sort
) { dialog, _ ->
setSort(
loyaltyCardOrders[currentIndex.get()]!!,
if (showReversed.isChecked) LoyaltyCardOrderDirection.Descending else LoyaltyCardOrderDirection.Ascending
)
ListWidget().updateAll(this@MainActivity)
dialog?.dismiss()
}
setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
}.create().show()
return true
}
R.id.action_manage_groups -> {
startActivity(
Intent(applicationContext, ManageGroupsActivity::class.java)
)
return true
}
R.id.action_import_export -> {
startActivity(
Intent(applicationContext, ImportExportActivity::class.java)
)
return true
}
R.id.action_settings -> {
mSettingsLauncher.launch(
Intent(applicationContext, SettingsActivity::class.java)
)
return true
}
R.id.action_about -> {
startActivity(
Intent(applicationContext, AboutActivity::class.java)
)
return true
}
}
return super.onOptionsItemSelected(inputItem)
}
private fun setSort(order: LoyaltyCardOrder, direction: LoyaltyCardOrderDirection) {
// Update values
mOrder = order
mOrderDirection = direction
// Store in Shared Preference to restore next app launch
applicationContext.getSharedPreferences(
getString(R.string.sharedpreference_sort),
MODE_PRIVATE
).edit {
putString(
getString(R.string.sharedpreference_sort_order),
order.name
)
putString(
getString(R.string.sharedpreference_sort_direction),
direction.name
)
}
// Update card list
updateLoyaltyCardList(false)
}
override fun onRowLongClicked(inputPosition: Int) {
enableActionMode(inputPosition)
}
private fun enableActionMode(inputPosition: Int) {
if (mCurrentActionMode == null) {
mCurrentActionMode = startSupportActionMode(mCurrentActionModeCallback)
}
toggleSelection(inputPosition)
}
private fun scaleScreen() {
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
val screenHeight = displayMetrics.heightPixels
val mediumSizePx = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
MEDIUM_SCALE_FACTOR_DIP.toFloat(),
getResources().displayMetrics
)
val shouldScaleSmaller = screenHeight < mediumSizePx
binding.include.welcomeIcon.visibility = if (shouldScaleSmaller) View.GONE else View.VISIBLE
}
private fun toggleSelection(inputPosition: Int) {
mAdapter.toggleSelection(inputPosition)
val count = mAdapter.selectedItemCount
if (count == 0) {
mCurrentActionMode!!.finish()
} else {
mCurrentActionMode!!.title = getResources().getQuantityString(
R.plurals.selectedCardCount,
count,
count
)
val editItem = mCurrentActionMode!!.menu.findItem(R.id.action_edit)
val duplicateItem = mCurrentActionMode!!.menu.findItem(R.id.action_duplicate)
val archiveItem = mCurrentActionMode!!.menu.findItem(R.id.action_archive)
val unarchiveItem = mCurrentActionMode!!.menu.findItem(R.id.action_unarchive)
val starItem = mCurrentActionMode!!.menu.findItem(R.id.action_star)
val unstarItem = mCurrentActionMode!!.menu.findItem(R.id.action_unstar)
var hasStarred = false
var hasUnstarred = false
var hasArchived = false
var hasUnarchived = false
for (loyaltyCard in mAdapter.getSelectedItems()) {
if (loyaltyCard.starStatus == 1) {
hasStarred = true
} else {
hasUnstarred = true
}
if (loyaltyCard.archiveStatus == 1) {
hasArchived = true
} else {
hasUnarchived = true
}
// We have all types, no need to keep checking
if (hasStarred && hasUnstarred && hasArchived && hasUnarchived) {
break
}
}
unarchiveItem.isVisible = hasArchived
archiveItem.isVisible = hasUnarchived
if (count == 1) {
starItem.isVisible = !hasStarred
unstarItem.isVisible = !hasUnstarred
editItem.isVisible = true
editItem.isEnabled = true
duplicateItem.isVisible = true
duplicateItem.isEnabled = true
} else {
starItem.isVisible = hasUnstarred
unstarItem.isVisible = hasStarred
editItem.isVisible = false
editItem.isEnabled = false
duplicateItem.isVisible = false
duplicateItem.isEnabled = false
}
mCurrentActionMode!!.invalidate()
}
}
override fun onRowClicked(inputPosition: Int) {
if (mAdapter.selectedItemCount > 0) {
enableActionMode(inputPosition)
} else {
// FIXME
//
// There is a really nasty edge case that can happen when someone taps a card but right
// after it swipes (very small window, hard to reproduce). The cursor gets replaced and
// may not have a card at the ID number that is returned from onRowClicked.
//
// The proper fix, obviously, would involve makes sure an onFling can't happen while a
// click is being processed. Sadly, I have not yet found a way to make that possible.
val loyaltyCard: LoyaltyCard
try {
loyaltyCard = mAdapter.getCard(inputPosition)
} catch (e: CursorIndexOutOfBoundsException) {
Log.w(TAG, "Prevented crash from tap + swipe on ID $inputPosition: $e")
return
}
startActivity(
Intent(this, LoyaltyCardViewActivity::class.java).apply {
action = ""
putExtras(Bundle().apply {
putInt(LoyaltyCardViewActivity.BUNDLE_ID, loyaltyCard.id)
val cardList = ArrayList<Int?>()
for (i in 0..<mAdapter.itemCount) {
cardList.add(mAdapter.getCard(i).id)
}
putIntegerArrayList(LoyaltyCardViewActivity.BUNDLE_CARDLIST, cardList)
})
}
)
}
}
companion object {
private const val TAG = "Catima"
const val RESTART_ACTIVITY_INTENT: String = "restart_activity_intent"
private const val MEDIUM_SCALE_FACTOR_DIP = 460
const val STATE_SEARCH_QUERY: String = "SEARCH_QUERY"
}
}