diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c77a4b604..8dff34dd3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -135,6 +135,7 @@ dependencies { implementation(projects.core.accounts) implementation(projects.core.common) implementation(projects.core.database) + implementation(projects.core.navigation) implementation(projects.core.network) implementation(projects.core.preferences) diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index 5bbfd9ed1..d7d98cce9 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -872,7 +872,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2049,7 +2049,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2060,7 +2060,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2071,7 +2071,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2082,7 +2082,7 @@ errorLine2=" ~~~~~~~"> @@ -2093,7 +2093,7 @@ errorLine2=" ~~~~~~~"> @@ -2104,7 +2104,7 @@ errorLine2=" ~~~~~~~"> @@ -2115,7 +2115,7 @@ errorLine2=" ~~~~~~~"> @@ -2126,7 +2126,7 @@ errorLine2=" ~~~~~~~"> @@ -2137,7 +2137,7 @@ errorLine2=" ~~~~~~~"> @@ -2148,7 +2148,7 @@ errorLine2=" ~~~~~~~~~~~~~"> @@ -2159,7 +2159,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -2170,7 +2170,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -2335,7 +2335,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -2346,7 +2346,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2357,7 +2357,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2445,7 +2445,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2456,7 +2456,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2467,7 +2467,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2478,7 +2478,7 @@ errorLine2=" ~~~~~~~~~"> @@ -2731,7 +2731,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> @@ -2742,7 +2742,7 @@ errorLine2=" ~~~~~~"> @@ -2775,7 +2775,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~"> @@ -2786,7 +2786,7 @@ errorLine2=" ~~~~~~~"> @@ -2797,7 +2797,7 @@ errorLine2=" ~~~~~~~"> @@ -2808,7 +2808,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -2819,7 +2819,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -2830,7 +2830,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -2841,7 +2841,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -2852,7 +2852,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -2863,7 +2863,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -2874,7 +2874,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2885,7 +2885,7 @@ errorLine2=" ~~~~~~~~~~~~~"> @@ -2896,7 +2896,7 @@ errorLine2=" ~~~~~~~"> @@ -2907,7 +2907,7 @@ errorLine2=" ~~~~~~~"> @@ -2918,7 +2918,7 @@ errorLine2=" ~~~~~~~"> @@ -2929,7 +2929,7 @@ errorLine2=" ~~~~~~~"> @@ -2940,7 +2940,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -2951,7 +2951,7 @@ errorLine2=" ~~~~~~~"> @@ -2962,7 +2962,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -2973,7 +2973,7 @@ errorLine2=" ~~~~~~~"> @@ -2984,7 +2984,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -2995,7 +2995,7 @@ errorLine2=" ~~~~~~~"> @@ -3006,7 +3006,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -3017,29 +3017,7 @@ errorLine2=" ~~~~~~~"> - - - - - - - - @@ -3072,7 +3050,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -3083,7 +3061,7 @@ errorLine2=" ~~~~~~~"> @@ -3105,18 +3083,18 @@ errorLine2=" ~~~~~~~"> + message="Access to `private` method `primaryDrawerItem` of class `MainActivityKt` requires synthetic accessor" + errorLine1=" primaryDrawerItem {" + errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -3127,7 +3105,7 @@ errorLine2=" ~~~~~~~"> @@ -3138,7 +3116,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3149,7 +3127,7 @@ errorLine2=" ~~~~~~~"> @@ -3160,7 +3138,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3171,7 +3149,7 @@ errorLine2=" ~~~~~~~"> @@ -3182,7 +3160,29 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + + + + + + + + @@ -3193,7 +3193,7 @@ errorLine2=" ~~~~~~~"> @@ -3204,7 +3204,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -3215,7 +3215,7 @@ errorLine2=" ~~~~~~~"> @@ -3226,7 +3226,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -3237,7 +3237,7 @@ errorLine2=" ~~~~~~~"> @@ -3248,7 +3248,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3259,7 +3259,7 @@ errorLine2=" ~~~~~~~"> @@ -3270,7 +3270,7 @@ errorLine2=" ~~~~~~~"> @@ -3281,7 +3281,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -3292,7 +3292,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -3534,7 +3534,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -3644,7 +3644,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3699,7 +3699,7 @@ errorLine2=" ~~~~~~~"> @@ -3710,7 +3710,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -3721,7 +3721,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -3732,7 +3732,7 @@ errorLine2=" ~~~~~~~"> @@ -3743,7 +3743,7 @@ errorLine2=" ~~~~~~~"> diff --git a/app/src/main/java/app/pachli/AboutActivity.kt b/app/src/main/java/app/pachli/AboutActivity.kt index df8b772c8..7e3152dfe 100644 --- a/app/src/main/java/app/pachli/AboutActivity.kt +++ b/app/src/main/java/app/pachli/AboutActivity.kt @@ -3,7 +3,6 @@ package app.pachli import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import android.content.Intent import android.os.Build import android.os.Bundle import android.text.SpannableString @@ -16,6 +15,8 @@ import android.widget.Toast import androidx.annotation.StringRes import androidx.lifecycle.lifecycleScope import app.pachli.components.instanceinfo.InstanceInfoRepository +import app.pachli.core.navigation.LicenseActivityIntent +import app.pachli.core.navigation.PrivacyPolicyActivityIntent import app.pachli.databinding.ActivityAboutBinding import app.pachli.util.NoUnderlineURLSpan import app.pachli.util.hide @@ -76,8 +77,7 @@ class AboutActivity : BottomSheetActivity() { binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site) binding.aboutPrivacyPolicyTextView.setOnClickListener { - val intent = Intent(this, PrivacyPolicyActivity::class.java) - startActivity(intent) + startActivity(PrivacyPolicyActivityIntent(this)) } binding.appProfileButton.setOnClickListener { @@ -85,7 +85,7 @@ class AboutActivity : BottomSheetActivity() { } binding.aboutLicensesButton.setOnClickListener { - startActivityWithSlideInAnimation(Intent(this, LicenseActivity::class.java)) + startActivityWithSlideInAnimation(LicenseActivityIntent(this)) } binding.copyDeviceInfo.setOnClickListener { diff --git a/app/src/main/java/app/pachli/BaseActivity.kt b/app/src/main/java/app/pachli/BaseActivity.kt index d12bb25f1..d3a69bc22 100644 --- a/app/src/main/java/app/pachli/BaseActivity.kt +++ b/app/src/main/java/app/pachli/BaseActivity.kt @@ -34,9 +34,9 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import app.pachli.MainActivity.Companion.redirectIntent import app.pachli.adapter.AccountSelectionAdapter -import app.pachli.components.login.LoginActivity import app.pachli.core.accounts.AccountManager import app.pachli.core.database.model.AccountEntity +import app.pachli.core.navigation.LoginActivityIntent import app.pachli.core.preferences.PrefKeys import app.pachli.core.preferences.PrefKeys.APP_THEME import app.pachli.core.preferences.SharedPreferencesRepository @@ -182,7 +182,7 @@ abstract class BaseActivity : AppCompatActivity() { private fun redirectIfNotLoggedIn() { val account = accountManager.activeAccount if (account == null) { - val intent = Intent(this, LoginActivity::class.java) + val intent = LoginActivityIntent(this) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) startActivityWithSlideInAnimation(intent) finish() diff --git a/app/src/main/java/app/pachli/BottomSheetActivity.kt b/app/src/main/java/app/pachli/BottomSheetActivity.kt index 169aba027..fc9d38b96 100644 --- a/app/src/main/java/app/pachli/BottomSheetActivity.kt +++ b/app/src/main/java/app/pachli/BottomSheetActivity.kt @@ -17,15 +17,14 @@ package app.pachli import android.content.Context -import android.content.Intent import android.os.Bundle import android.view.View import android.widget.LinearLayout import android.widget.Toast import androidx.annotation.VisibleForTesting import androidx.lifecycle.lifecycleScope -import app.pachli.components.account.AccountActivity -import app.pachli.components.viewthread.ViewThreadActivity +import app.pachli.core.navigation.AccountActivityIntent +import app.pachli.core.navigation.ViewThreadActivityIntent import app.pachli.core.network.retrofit.MastodonApi import app.pachli.util.looksLikeMastodonUrl import app.pachli.util.openLink @@ -104,15 +103,13 @@ abstract class BottomSheetActivity : BaseActivity() { open fun viewThread(statusId: String, url: String?) { if (!isSearching()) { - val intent = Intent(this, ViewThreadActivity::class.java) - intent.putExtra("id", statusId) - intent.putExtra("url", url) + val intent = ViewThreadActivityIntent(this, statusId, url) startActivityWithSlideInAnimation(intent) } } open fun viewAccount(id: String) { - val intent = AccountActivity.getIntent(this, id) + val intent = AccountActivityIntent(this, id) startActivityWithSlideInAnimation(intent) } diff --git a/app/src/main/java/app/pachli/ListsActivity.kt b/app/src/main/java/app/pachli/ListsActivity.kt index 4ac77bacd..14879e191 100644 --- a/app/src/main/java/app/pachli/ListsActivity.kt +++ b/app/src/main/java/app/pachli/ListsActivity.kt @@ -18,8 +18,6 @@ package app.pachli import android.app.Dialog -import android.content.Context -import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -36,6 +34,7 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import app.pachli.core.navigation.StatusListActivityIntent import app.pachli.core.network.model.MastoList import app.pachli.databinding.ActivityListsBinding import app.pachli.databinding.DialogListBinding @@ -191,7 +190,7 @@ class ListsActivity : BaseActivity() { private fun onListSelected(listId: String, listTitle: String) { startActivityWithSlideInAnimation( - StatusListActivity.newListIntent(this, listId, listTitle), + StatusListActivityIntent.list(this, listId, listTitle), ) } @@ -277,8 +276,4 @@ class ListsActivity : BaseActivity() { viewModel.updateList(listId, name, exclusive) } } - - companion object { - fun newIntent(context: Context) = Intent(context, ListsActivity::class.java) - } } diff --git a/app/src/main/java/app/pachli/MainActivity.kt b/app/src/main/java/app/pachli/MainActivity.kt index f71615f29..16390e368 100644 --- a/app/src/main/java/app/pachli/MainActivity.kt +++ b/app/src/main/java/app/pachli/MainActivity.kt @@ -46,7 +46,6 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.content.res.AppCompatResources import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.app.ActivityCompat -import androidx.core.content.IntentCompat import androidx.core.content.edit import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.res.ResourcesCompat @@ -61,24 +60,32 @@ import app.pachli.appstore.CacheUpdater import app.pachli.appstore.EventHub import app.pachli.appstore.MainTabsChangedEvent import app.pachli.appstore.ProfileEditedEvent -import app.pachli.components.account.AccountActivity -import app.pachli.components.accountlist.AccountListActivity -import app.pachli.components.announcements.AnnouncementsActivity -import app.pachli.components.compose.ComposeActivity import app.pachli.components.compose.ComposeActivity.Companion.canHandleMimeType -import app.pachli.components.drafts.DraftsActivity -import app.pachli.components.login.LoginActivity import app.pachli.components.notifications.createNotificationChannelsForAccount import app.pachli.components.notifications.disableAllNotifications import app.pachli.components.notifications.enablePushNotificationsWithFallback import app.pachli.components.notifications.notificationsAreEnabled import app.pachli.components.notifications.showMigrationNoticeIfNecessary -import app.pachli.components.preference.PreferencesActivity -import app.pachli.components.scheduled.ScheduledStatusActivity -import app.pachli.components.search.SearchActivity -import app.pachli.components.trending.TrendingActivity import app.pachli.core.database.model.AccountEntity import app.pachli.core.database.model.TabKind +import app.pachli.core.navigation.AboutActivityIntent +import app.pachli.core.navigation.AccountActivityIntent +import app.pachli.core.navigation.AccountListActivityIntent +import app.pachli.core.navigation.AnnouncementsActivityIntent +import app.pachli.core.navigation.ComposeActivityIntent +import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions +import app.pachli.core.navigation.DraftsActivityIntent +import app.pachli.core.navigation.EditProfileActivityIntent +import app.pachli.core.navigation.ListActivityIntent +import app.pachli.core.navigation.LoginActivityIntent +import app.pachli.core.navigation.LoginActivityIntent.LoginMode +import app.pachli.core.navigation.MainActivityIntent +import app.pachli.core.navigation.PreferencesActivityIntent +import app.pachli.core.navigation.PreferencesActivityIntent.PreferenceScreen +import app.pachli.core.navigation.ScheduledStatusActivityIntent +import app.pachli.core.navigation.SearchActivityIntent +import app.pachli.core.navigation.StatusListActivityIntent +import app.pachli.core.navigation.TrendingActivityIntent import app.pachli.core.network.model.Account import app.pachli.core.network.model.Notification import app.pachli.core.preferences.PrefKeys @@ -253,13 +260,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { ) } } else if (openDrafts) { - val intent = DraftsActivity.newIntent(this) + val intent = DraftsActivityIntent(this) startActivity(intent) } else if (accountRequested && intent.hasExtra(NOTIFICATION_TYPE)) { // user clicked a notification, show follow requests for type FOLLOW_REQUEST, // otherwise show notification tab if (intent.getSerializableExtra(NOTIFICATION_TYPE) == Notification.Type.FOLLOW_REQUEST) { - val intent = AccountListActivity.newIntent(this, AccountListActivity.Type.FOLLOW_REQUESTS) + val intent = AccountListActivityIntent(this, AccountListActivityIntent.Kind.FOLLOW_REQUESTS) startActivityWithSlideInAnimation(intent) } else { showNotificationTab = true @@ -272,7 +279,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { glide = Glide.with(this) binding.composeButton.setOnClickListener { - val composeIntent = Intent(applicationContext, ComposeActivity::class.java) + val composeIntent = ComposeActivityIntent(applicationContext) startActivity(composeIntent) } @@ -393,7 +400,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { override fun onMenuItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_search -> { - startActivity(SearchActivity.getIntent(this@MainActivity)) + startActivity(SearchActivityIntent(this@MainActivity)) true } else -> super.onOptionsItemSelected(item) @@ -503,7 +510,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { return true } KeyEvent.KEYCODE_SEARCH -> { - startActivityWithSlideInAnimation(SearchActivity.getIntent(this)) + startActivityWithSlideInAnimation(SearchActivityIntent(this)) return true } } @@ -512,7 +519,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { when (keyCode) { KeyEvent.KEYCODE_N -> { // open compose activity by pressing SHIFT + N (or CTRL + N) - val composeIntent = Intent(applicationContext, ComposeActivity::class.java) + val composeIntent = ComposeActivityIntent(applicationContext) startActivity(composeIntent) return true } @@ -533,12 +540,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { } private fun forwardToComposeActivity(intent: Intent) { - val composeOptions = IntentCompat.getParcelableExtra(intent, COMPOSE_OPTIONS, ComposeActivity.ComposeOptions::class.java) + val composeOptions = ComposeActivityIntent.getOptions(intent) val composeIntent = if (composeOptions != null) { - ComposeActivity.startIntent(this, composeOptions) + ComposeActivityIntent(this, composeOptions) } else { - Intent(this, ComposeActivity::class.java).apply { + ComposeActivityIntent(this).apply { action = intent.action type = intent.type putExtras(intent) @@ -640,7 +647,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { nameRes = R.string.action_edit_profile iconicsIcon = GoogleMaterial.Icon.gmd_person onClick = { - val intent = Intent(context, EditProfileActivity::class.java) + val intent = EditProfileActivityIntent(context) startActivityWithSlideInAnimation(intent) } }, @@ -649,7 +656,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { isSelectable = false iconicsIcon = GoogleMaterial.Icon.gmd_star onClick = { - val intent = StatusListActivity.newFavouritesIntent(context) + val intent = StatusListActivityIntent.favourites(context) startActivityWithSlideInAnimation(intent) } }, @@ -657,7 +664,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { nameRes = R.string.action_view_bookmarks iconicsIcon = GoogleMaterial.Icon.gmd_bookmark onClick = { - val intent = StatusListActivity.newBookmarksIntent(context) + val intent = StatusListActivityIntent.bookmarks(context) startActivityWithSlideInAnimation(intent) } }, @@ -665,7 +672,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { nameRes = R.string.action_view_follow_requests iconicsIcon = GoogleMaterial.Icon.gmd_person_add onClick = { - val intent = AccountListActivity.newIntent(context, AccountListActivity.Type.FOLLOW_REQUESTS) + val intent = AccountListActivityIntent(context, AccountListActivityIntent.Kind.FOLLOW_REQUESTS) startActivityWithSlideInAnimation(intent) } }, @@ -673,14 +680,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { nameRes = R.string.action_lists iconicsIcon = GoogleMaterial.Icon.gmd_list onClick = { - startActivityWithSlideInAnimation(ListsActivity.newIntent(context)) + startActivityWithSlideInAnimation(ListActivityIntent(context)) } }, primaryDrawerItem { nameRes = R.string.action_access_drafts iconRes = R.drawable.ic_notebook onClick = { - val intent = DraftsActivity.newIntent(context) + val intent = DraftsActivityIntent(context) startActivityWithSlideInAnimation(intent) } }, @@ -688,7 +695,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { nameRes = R.string.action_access_scheduled_posts iconRes = R.drawable.ic_access_time onClick = { - startActivityWithSlideInAnimation(ScheduledStatusActivity.newIntent(context)) + startActivityWithSlideInAnimation(ScheduledStatusActivityIntent(context)) } }, primaryDrawerItem { @@ -696,7 +703,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { nameRes = R.string.title_announcements iconRes = R.drawable.ic_bullhorn_24dp onClick = { - startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context)) + startActivityWithSlideInAnimation(AnnouncementsActivityIntent(context)) } badgeStyle = BadgeStyle().apply { textColor = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, com.google.android.material.R.attr.colorOnPrimary)) @@ -708,7 +715,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { nameRes = R.string.action_view_account_preferences iconRes = R.drawable.ic_account_settings onClick = { - val intent = PreferencesActivity.newIntent(context, PreferencesActivity.ACCOUNT_PREFERENCES) + val intent = PreferencesActivityIntent(context, PreferenceScreen.ACCOUNT) startActivityWithSlideInAnimation(intent) } }, @@ -716,7 +723,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { nameRes = R.string.action_view_preferences iconicsIcon = GoogleMaterial.Icon.gmd_settings onClick = { - val intent = PreferencesActivity.newIntent(context, PreferencesActivity.GENERAL_PREFERENCES) + val intent = PreferencesActivityIntent(context, PreferenceScreen.GENERAL) startActivityWithSlideInAnimation(intent) } }, @@ -724,7 +731,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { nameRes = R.string.about_title_activity iconicsIcon = GoogleMaterial.Icon.gmd_info onClick = { - val intent = Intent(context, AboutActivity::class.java) + val intent = AboutActivityIntent(context) startActivityWithSlideInAnimation(intent) } }, @@ -742,7 +749,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { nameRes = R.string.action_search iconicsIcon = GoogleMaterial.Icon.gmd_search onClick = { - startActivityWithSlideInAnimation(SearchActivity.getIntent(context)) + startActivityWithSlideInAnimation(SearchActivityIntent(context)) } }, ) @@ -754,7 +761,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { nameRes = R.string.title_public_trending iconicsIcon = GoogleMaterial.Icon.gmd_trending_up onClick = { - startActivityWithSlideInAnimation(TrendingActivity.getIntent(context)) + startActivityWithSlideInAnimation(TrendingActivityIntent(context)) } }, ) @@ -941,13 +948,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { // open profile when active image was clicked if (current && activeAccount != null) { - val intent = AccountActivity.getIntent(this, activeAccount.accountId) + val intent = AccountActivityIntent(this, activeAccount.accountId) startActivityWithSlideInAnimation(intent) return } // open LoginActivity to add new account if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) { - startActivityWithSlideInAnimation(LoginActivity.getIntent(this, LoginActivity.MODE_ADDITIONAL_LOGIN)) + startActivityWithSlideInAnimation( + LoginActivityIntent(this, LoginMode.ADDITIONAL_LOGIN), + ) return } // change Account @@ -958,7 +967,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { private fun changeAccount(newSelectedId: Long, forward: Intent?) { cacheUpdater.stop() accountManager.setActiveAccount(newSelectedId) - val intent = Intent(this, MainActivity::class.java) + val intent = MainActivityIntent(this) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK if (forward != null) { intent.type = forward.type @@ -988,9 +997,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { lifecycleScope.launch { val otherAccountAvailable = logoutUsecase.logout() val intent = if (otherAccountAvailable) { - Intent(this@MainActivity, MainActivity::class.java) + MainActivityIntent(this@MainActivity) } else { - LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT) + LoginActivityIntent(this@MainActivity, LoginMode.DEFAULT) } startActivity(intent) finishWithoutSlideOutAnimation() @@ -1197,7 +1206,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { */ @JvmStatic fun accountSwitchIntent(context: Context, pachliAccountId: Long): Intent { - return Intent(context, MainActivity::class.java).apply { + return MainActivityIntent(context).apply { putExtra(PACHLI_ACCOUNT_ID, pachliAccountId) } } @@ -1221,7 +1230,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { @JvmStatic fun composeIntent( context: Context, - options: ComposeActivity.ComposeOptions, + options: ComposeOptions, pachliAccountId: Long = -1, notificationTag: String? = null, notificationId: Int = -1, diff --git a/app/src/main/java/app/pachli/SplashActivity.kt b/app/src/main/java/app/pachli/SplashActivity.kt index 0d966fc45..862c70315 100644 --- a/app/src/main/java/app/pachli/SplashActivity.kt +++ b/app/src/main/java/app/pachli/SplashActivity.kt @@ -18,12 +18,13 @@ package app.pachli import android.annotation.SuppressLint -import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import app.pachli.components.login.LoginActivity import app.pachli.core.accounts.AccountManager +import app.pachli.core.navigation.LoginActivityIntent +import app.pachli.core.navigation.LoginActivityIntent.LoginMode +import app.pachli.core.navigation.MainActivityIntent import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -41,9 +42,9 @@ class SplashActivity : AppCompatActivity() { /** Determine whether the user is currently logged in, and if so go ahead and load the * timeline. Otherwise, start the activity_login screen. */ val intent = if (accountManager.activeAccount != null) { - Intent(this, MainActivity::class.java) + MainActivityIntent(this) } else { - LoginActivity.getIntent(this, LoginActivity.MODE_DEFAULT) + LoginActivityIntent(this, LoginMode.DEFAULT) } startActivity(intent) finish() diff --git a/app/src/main/java/app/pachli/StatusListActivity.kt b/app/src/main/java/app/pachli/StatusListActivity.kt index b968bb457..828b1f356 100644 --- a/app/src/main/java/app/pachli/StatusListActivity.kt +++ b/app/src/main/java/app/pachli/StatusListActivity.kt @@ -17,8 +17,6 @@ package app.pachli -import android.content.Context -import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem @@ -26,8 +24,10 @@ import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope import app.pachli.appstore.EventHub import app.pachli.appstore.FilterChangedEvent -import app.pachli.components.compose.ComposeActivity import app.pachli.components.timeline.TimelineFragment +import app.pachli.core.navigation.ComposeActivityIntent +import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions +import app.pachli.core.navigation.StatusListActivityIntent import app.pachli.core.network.model.Filter import app.pachli.core.network.model.FilterV1 import app.pachli.core.network.model.TimelineKind @@ -84,7 +84,7 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton setSupportActionBar(binding.includedToolbar.toolbar) - timelineKind = intent.getParcelableExtra(EXTRA_KIND)!! + timelineKind = StatusListActivityIntent.getKind(intent) val title = when (timelineKind) { is TimelineKind.Favourites -> getString(R.string.title_favourites) @@ -113,11 +113,11 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton val composeIntent = when (timelineKind) { is TimelineKind.Tag -> { val tag = (timelineKind as TimelineKind.Tag).tags.first() - ComposeActivity.startIntent( + ComposeActivityIntent( this, - ComposeActivity.ComposeOptions( + ComposeOptions( content = getString(R.string.title_tag_with_initial_position).format(tag), - initialCursorPosition = ComposeActivity.InitialCursorPosition.START, + initialCursorPosition = ComposeOptions.InitialCursorPosition.START, ), ) } @@ -125,10 +125,7 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton is TimelineKind.Favourites, is TimelineKind.UserList, -> { - ComposeActivity.startIntent( - this, - ComposeActivity.ComposeOptions(), - ) + ComposeActivityIntent(this, ComposeOptions()) } else -> null } @@ -367,29 +364,4 @@ class StatusListActivity : BottomSheetActivity(), AppBarLayoutHost, ActionButton return true } - - companion object { - private const val EXTRA_KIND = "kind" - - fun newFavouritesIntent(context: Context) = - Intent(context, StatusListActivity::class.java).apply { - putExtra(EXTRA_KIND, TimelineKind.Favourites) - } - - fun newBookmarksIntent(context: Context) = - Intent(context, StatusListActivity::class.java).apply { - putExtra(EXTRA_KIND, TimelineKind.Bookmarks) - } - - fun newListIntent(context: Context, listId: String, listTitle: String) = - Intent(context, StatusListActivity::class.java).apply { - putExtra(EXTRA_KIND, TimelineKind.UserList(listId, listTitle)) - } - - @JvmStatic - fun newHashtagIntent(context: Context, hashtag: String) = - Intent(context, StatusListActivity::class.java).apply { - putExtra(EXTRA_KIND, TimelineKind.Tag(listOf(hashtag))) - } - } } diff --git a/app/src/main/java/app/pachli/TabPreferenceActivity.kt b/app/src/main/java/app/pachli/TabPreferenceActivity.kt index 9dd740b16..a4b9e081d 100644 --- a/app/src/main/java/app/pachli/TabPreferenceActivity.kt +++ b/app/src/main/java/app/pachli/TabPreferenceActivity.kt @@ -17,7 +17,6 @@ package app.pachli -import android.content.Intent import android.graphics.Color import android.os.Bundle import android.view.Gravity @@ -44,6 +43,7 @@ import app.pachli.appstore.EventHub import app.pachli.appstore.MainTabsChangedEvent import app.pachli.core.database.model.TabData import app.pachli.core.database.model.TabKind +import app.pachli.core.navigation.ListActivityIntent import app.pachli.core.network.model.MastoList import app.pachli.core.network.retrofit.MastodonApi import app.pachli.databinding.ActivityTabPreferenceBinding @@ -303,7 +303,7 @@ class TabPreferenceActivity : BaseActivity(), ItemInteractionListener { val dialogBuilder = AlertDialog.Builder(this) .setTitle(R.string.select_list_title) .setNeutralButton(R.string.select_list_manage) { _, _ -> - val listIntent = Intent(applicationContext, ListsActivity::class.java) + val listIntent = ListActivityIntent(applicationContext) startActivity(listIntent) } .setNegativeButton(android.R.string.cancel, null) diff --git a/app/src/main/java/app/pachli/ViewMediaActivity.kt b/app/src/main/java/app/pachli/ViewMediaActivity.kt index 9e12e3b3f..990a56f0b 100644 --- a/app/src/main/java/app/pachli/ViewMediaActivity.kt +++ b/app/src/main/java/app/pachli/ViewMediaActivity.kt @@ -24,7 +24,6 @@ import android.app.DownloadManager import android.content.ClipData import android.content.ClipboardManager import android.content.Context -import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.Color @@ -40,13 +39,14 @@ import android.webkit.MimeTypeMap import android.widget.Toast import androidx.core.app.ShareCompat import androidx.core.content.FileProvider -import androidx.core.content.IntentCompat import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import app.pachli.BuildConfig.APPLICATION_ID -import app.pachli.components.viewthread.ViewThreadActivity +import app.pachli.core.navigation.AttachmentViewData +import app.pachli.core.navigation.ViewMediaActivityIntent +import app.pachli.core.navigation.ViewThreadActivityIntent import app.pachli.core.network.model.Attachment import app.pachli.databinding.ActivityViewMediaBinding import app.pachli.fragment.ViewImageFragment @@ -55,7 +55,6 @@ import app.pachli.pager.ImagePagerAdapter import app.pachli.pager.SingleImagePagerAdapter import app.pachli.util.getTemporaryMediaFilename import app.pachli.util.viewBinding -import app.pachli.viewdata.AttachmentViewData import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider import autodispose2.autoDispose import com.bumptech.glide.Glide @@ -73,6 +72,9 @@ import java.util.Locale typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit +/** + * Show one or more media items (pictures, video, audio, etc). + */ @AndroidEntryPoint class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener, ViewVideoFragment.VideoActionsListener { private val binding by viewBinding(ActivityViewMediaBinding::inflate) @@ -100,8 +102,8 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener supportPostponeEnterTransition() // Gather the parameters. - attachments = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_ATTACHMENTS, AttachmentViewData::class.java) - val initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0) + attachments = ViewMediaActivityIntent.getAttachments(intent) + val initialPosition = ViewMediaActivityIntent.getAttachmentIndex(intent) // Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener // but it cannot be expressed and if I don't specify type explicitly compilation fails @@ -111,7 +113,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener // Setup the view pager. ImagePagerAdapter(this, realAttachs, initialPosition) } else { - imageUrl = intent.getStringExtra(EXTRA_SINGLE_IMAGE_URL) + imageUrl = ViewMediaActivityIntent.getImageUrl(intent) ?: throw IllegalArgumentException("attachment list or image url has to be set") SingleImagePagerAdapter(this, imageUrl!!) @@ -241,7 +243,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener private fun onOpenStatus() { val attach = attachments!![binding.viewPager.currentItem] - startActivityWithSlideInAnimation(ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl)) + startActivityWithSlideInAnimation(ViewThreadActivityIntent(this, attach.statusId, attach.statusUrl)) } private fun copyLink() { @@ -344,27 +346,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener shareFile(file, mimeType) } - - companion object { - private const val EXTRA_ATTACHMENTS = "attachments" - private const val EXTRA_ATTACHMENT_INDEX = "index" - private const val EXTRA_SINGLE_IMAGE_URL = "single_image" - - @JvmStatic - fun newIntent(context: Context?, attachments: List, index: Int): Intent { - val intent = Intent(context, ViewMediaActivity::class.java) - intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments)) - intent.putExtra(EXTRA_ATTACHMENT_INDEX, index) - return intent - } - - @JvmStatic - fun newSingleImageIntent(context: Context, url: String): Intent { - val intent = Intent(context, ViewMediaActivity::class.java) - intent.putExtra(EXTRA_SINGLE_IMAGE_URL, url) - return intent - } - } } abstract class ViewMediaAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) { diff --git a/app/src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt b/app/src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt index 6504a6c7a..4698dc058 100644 --- a/app/src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt +++ b/app/src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt @@ -20,10 +20,10 @@ import androidx.core.content.ContextCompat import androidx.core.text.HtmlCompat import androidx.recyclerview.widget.RecyclerView import app.pachli.R -import app.pachli.ViewMediaActivity.Companion.newSingleImageIntent import app.pachli.core.common.util.AbsoluteTimeFormatter import app.pachli.core.common.util.formatNumber import app.pachli.core.database.model.TranslationState +import app.pachli.core.navigation.ViewMediaActivityIntent import app.pachli.core.network.model.Attachment import app.pachli.core.network.model.Emoji import app.pachli.core.network.model.PreviewCardKind @@ -877,7 +877,7 @@ abstract class StatusBaseViewHolder protected constructor(itemView: View) : cardView.bind(card, status.actionable.sensitive, statusDisplayOptions) { target -> if (card.kind == PreviewCardKind.PHOTO && card.embedUrl.isNotEmpty() && target == PreviewCardView.Target.IMAGE) { context.startActivity( - newSingleImageIntent(context, card.embedUrl), + ViewMediaActivityIntent(context, card.embedUrl), ) } else { listener.onViewUrl(card.url) diff --git a/app/src/main/java/app/pachli/components/account/AccountActivity.kt b/app/src/main/java/app/pachli/components/account/AccountActivity.kt index 66c6cb319..544d53307 100644 --- a/app/src/main/java/app/pachli/components/account/AccountActivity.kt +++ b/app/src/main/java/app/pachli/components/account/AccountActivity.kt @@ -47,15 +47,17 @@ import androidx.core.widget.doAfterTextChanged import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.MarginPageTransformer import app.pachli.BottomSheetActivity -import app.pachli.EditProfileActivity import app.pachli.R -import app.pachli.StatusListActivity -import app.pachli.ViewMediaActivity import app.pachli.components.account.list.ListsForAccountFragment -import app.pachli.components.accountlist.AccountListActivity -import app.pachli.components.compose.ComposeActivity -import app.pachli.components.report.ReportActivity import app.pachli.core.database.model.AccountEntity +import app.pachli.core.navigation.AccountActivityIntent +import app.pachli.core.navigation.AccountListActivityIntent +import app.pachli.core.navigation.ComposeActivityIntent +import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions +import app.pachli.core.navigation.EditProfileActivityIntent +import app.pachli.core.navigation.ReportActivityIntent +import app.pachli.core.navigation.StatusListActivityIntent +import app.pachli.core.navigation.ViewMediaActivityIntent import app.pachli.core.network.model.Account import app.pachli.core.network.model.Relationship import app.pachli.core.network.parseAsMastodonHtml @@ -100,6 +102,9 @@ import java.util.Locale import javax.inject.Inject import kotlin.math.abs +/** + * Show a single account's profile details. + */ @AndroidEntryPoint class AccountActivity : BottomSheetActivity(), @@ -172,7 +177,7 @@ class AccountActivity : addMenuProvider(this) // Obtain information to fill out the profile. - viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!) + viewModel.setAccountInfo(AccountActivityIntent.getAccountId(intent)) animateAvatar = sharedPreferencesRepository.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) animateEmojis = sharedPreferencesRepository.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) @@ -221,12 +226,12 @@ class AccountActivity : binding.accountFieldList.adapter = accountFieldAdapter val accountListClickListener = { v: View -> - val type = when (v.id) { - R.id.accountFollowers -> AccountListActivity.Type.FOLLOWERS - R.id.accountFollowing -> AccountListActivity.Type.FOLLOWS + val kind = when (v.id) { + R.id.accountFollowers -> AccountListActivityIntent.Kind.FOLLOWERS + R.id.accountFollowing -> AccountListActivityIntent.Kind.FOLLOWS else -> throw AssertionError() } - val accountListIntent = AccountListActivity.newIntent(this, type, viewModel.accountId) + val accountListIntent = AccountListActivityIntent(this, kind, viewModel.accountId) startActivityWithSlideInAnimation(accountListIntent) } binding.accountFollowers.setOnClickListener(accountListClickListener) @@ -540,7 +545,7 @@ class AccountActivity : private fun viewImage(view: View, uri: String) { view.transitionName = uri startActivity( - ViewMediaActivity.newSingleImageIntent(view.context, uri), + ViewMediaActivityIntent(view.context, uri), ActivityOptionsCompat.makeSceneTransitionAnimation(this, view, uri).toBundle(), ) } @@ -606,7 +611,7 @@ class AccountActivity : binding.accountFollowButton.setOnClickListener { if (viewModel.isSelf) { - val intent = Intent(this@AccountActivity, EditProfileActivity::class.java) + val intent = EditProfileActivityIntent(this@AccountActivity) startActivity(intent) return@setOnClickListener } @@ -879,26 +884,25 @@ class AccountActivity : private fun mention() { loadedAccount?.let { val options = if (viewModel.isSelf) { - ComposeActivity.ComposeOptions(kind = ComposeActivity.ComposeKind.NEW) + ComposeOptions(kind = ComposeOptions.ComposeKind.NEW) } else { - ComposeActivity.ComposeOptions( + ComposeOptions( mentionedUsernames = setOf(it.username), - kind = ComposeActivity.ComposeKind.NEW, + kind = ComposeOptions.ComposeKind.NEW, ) } - val intent = ComposeActivity.startIntent(this, options) + val intent = ComposeActivityIntent(this, options) startActivity(intent) } } override fun onViewTag(tag: String) { - val intent = StatusListActivity.newHashtagIntent(this, tag) + val intent = StatusListActivityIntent.hashtag(this, tag) startActivityWithSlideInAnimation(intent) } override fun onViewAccount(id: String) { - val intent = Intent(this, AccountActivity::class.java) - intent.putExtra("id", id) + val intent = AccountActivityIntent(this, id) startActivityWithSlideInAnimation(intent) } @@ -979,7 +983,7 @@ class AccountActivity : } R.id.action_report -> { loadedAccount?.let { loadedAccount -> - startActivity(ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username)) + startActivity(ReportActivityIntent(this, viewModel.accountId, loadedAccount.username)) } return true } @@ -999,14 +1003,6 @@ class AccountActivity : } companion object { - private const val KEY_ACCOUNT_ID = "id" private val argbEvaluator = ArgbEvaluator() - - @JvmStatic - fun getIntent(context: Context, accountId: String): Intent { - val intent = Intent(context, AccountActivity::class.java) - intent.putExtra(KEY_ACCOUNT_ID, accountId) - return intent - } } } diff --git a/app/src/main/java/app/pachli/components/account/media/AccountMediaFragment.kt b/app/src/main/java/app/pachli/components/account/media/AccountMediaFragment.kt index bd7526c4c..e3b1c46d2 100644 --- a/app/src/main/java/app/pachli/components/account/media/AccountMediaFragment.kt +++ b/app/src/main/java/app/pachli/components/account/media/AccountMediaFragment.kt @@ -32,8 +32,9 @@ import androidx.paging.LoadState import androidx.recyclerview.widget.GridLayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import app.pachli.R -import app.pachli.ViewMediaActivity import app.pachli.core.accounts.AccountManager +import app.pachli.core.navigation.AttachmentViewData +import app.pachli.core.navigation.ViewMediaActivityIntent import app.pachli.core.network.model.Attachment import app.pachli.core.preferences.PrefKeys import app.pachli.core.preferences.SharedPreferencesRepository @@ -43,7 +44,6 @@ import app.pachli.util.hide import app.pachli.util.openLink import app.pachli.util.show import app.pachli.util.viewBinding -import app.pachli.viewdata.AttachmentViewData import com.google.android.material.color.MaterialColors import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial @@ -175,7 +175,7 @@ class AccountMediaFragment : Attachment.Type.VIDEO, Attachment.Type.AUDIO, -> { - val intent = ViewMediaActivity.newIntent(context, attachmentsFromSameStatus, currentIndex) + val intent = ViewMediaActivityIntent(requireContext(), attachmentsFromSameStatus, currentIndex) if (activity != null) { val url = selected.attachment.url ViewCompat.setTransitionName(view, url) diff --git a/app/src/main/java/app/pachli/components/account/media/AccountMediaGridAdapter.kt b/app/src/main/java/app/pachli/components/account/media/AccountMediaGridAdapter.kt index 607d84f9a..4705fe945 100644 --- a/app/src/main/java/app/pachli/components/account/media/AccountMediaGridAdapter.kt +++ b/app/src/main/java/app/pachli/components/account/media/AccountMediaGridAdapter.kt @@ -11,6 +11,7 @@ import androidx.core.view.setPadding import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import app.pachli.R +import app.pachli.core.navigation.AttachmentViewData import app.pachli.core.network.model.Attachment import app.pachli.databinding.ItemAccountMediaBinding import app.pachli.util.BindingHolder @@ -18,7 +19,6 @@ import app.pachli.util.decodeBlurHash import app.pachli.util.getFormattedDescription import app.pachli.util.hide import app.pachli.util.show -import app.pachli.viewdata.AttachmentViewData import com.bumptech.glide.Glide import com.google.android.material.color.MaterialColors import java.util.Random diff --git a/app/src/main/java/app/pachli/components/account/media/AccountMediaPagingSource.kt b/app/src/main/java/app/pachli/components/account/media/AccountMediaPagingSource.kt index ae294cc93..ede39c679 100644 --- a/app/src/main/java/app/pachli/components/account/media/AccountMediaPagingSource.kt +++ b/app/src/main/java/app/pachli/components/account/media/AccountMediaPagingSource.kt @@ -18,7 +18,7 @@ package app.pachli.components.account.media import androidx.paging.PagingSource import androidx.paging.PagingState -import app.pachli.viewdata.AttachmentViewData +import app.pachli.core.navigation.AttachmentViewData class AccountMediaPagingSource( private val viewModel: AccountMediaViewModel, diff --git a/app/src/main/java/app/pachli/components/account/media/AccountMediaRemoteMediator.kt b/app/src/main/java/app/pachli/components/account/media/AccountMediaRemoteMediator.kt index c11524df2..dc3604f74 100644 --- a/app/src/main/java/app/pachli/components/account/media/AccountMediaRemoteMediator.kt +++ b/app/src/main/java/app/pachli/components/account/media/AccountMediaRemoteMediator.kt @@ -22,8 +22,8 @@ import androidx.paging.PagingState import androidx.paging.RemoteMediator import app.pachli.components.timeline.util.ifExpected import app.pachli.core.database.model.AccountEntity +import app.pachli.core.navigation.AttachmentViewData import app.pachli.core.network.retrofit.MastodonApi -import app.pachli.viewdata.AttachmentViewData import retrofit2.HttpException @OptIn(ExperimentalPagingApi::class) diff --git a/app/src/main/java/app/pachli/components/account/media/AccountMediaViewModel.kt b/app/src/main/java/app/pachli/components/account/media/AccountMediaViewModel.kt index 40c71b636..b591cc33f 100644 --- a/app/src/main/java/app/pachli/components/account/media/AccountMediaViewModel.kt +++ b/app/src/main/java/app/pachli/components/account/media/AccountMediaViewModel.kt @@ -23,8 +23,8 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn import app.pachli.core.accounts.AccountManager +import app.pachli.core.navigation.AttachmentViewData import app.pachli.core.network.retrofit.MastodonApi -import app.pachli.viewdata.AttachmentViewData import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject diff --git a/app/src/main/java/app/pachli/components/accountlist/AccountListActivity.kt b/app/src/main/java/app/pachli/components/accountlist/AccountListActivity.kt index 740c044ef..7cc90e4d6 100644 --- a/app/src/main/java/app/pachli/components/accountlist/AccountListActivity.kt +++ b/app/src/main/java/app/pachli/components/accountlist/AccountListActivity.kt @@ -16,18 +16,27 @@ package app.pachli.components.accountlist -import android.content.Context -import android.content.Intent import android.os.Bundle import androidx.fragment.app.commit import app.pachli.BottomSheetActivity import app.pachli.R +import app.pachli.core.navigation.AccountListActivityIntent +import app.pachli.core.navigation.AccountListActivityIntent.Kind.BLOCKS +import app.pachli.core.navigation.AccountListActivityIntent.Kind.FAVOURITED +import app.pachli.core.navigation.AccountListActivityIntent.Kind.FOLLOWERS +import app.pachli.core.navigation.AccountListActivityIntent.Kind.FOLLOWS +import app.pachli.core.navigation.AccountListActivityIntent.Kind.FOLLOW_REQUESTS +import app.pachli.core.navigation.AccountListActivityIntent.Kind.MUTES +import app.pachli.core.navigation.AccountListActivityIntent.Kind.REBLOGGED import app.pachli.databinding.ActivityAccountListBinding import app.pachli.interfaces.AppBarLayoutHost import app.pachli.util.viewBinding import com.google.android.material.appbar.AppBarLayout import dagger.hilt.android.AndroidEntryPoint +/** + * Show a list of accounts of a particular kind. + */ @AndroidEntryPoint class AccountListActivity : BottomSheetActivity(), AppBarLayoutHost { private val binding: ActivityAccountListBinding by viewBinding(ActivityAccountListBinding::inflate) @@ -35,52 +44,30 @@ class AccountListActivity : BottomSheetActivity(), AppBarLayoutHost { override val appBarLayout: AppBarLayout get() = binding.includedToolbar.appbar - enum class Type { - FOLLOWS, - FOLLOWERS, - BLOCKS, - MUTES, - FOLLOW_REQUESTS, - REBLOGGED, - FAVOURITED, - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) - val type = intent.getSerializableExtra(EXTRA_TYPE) as Type - val id: String? = intent.getStringExtra(EXTRA_ID) + val kind = AccountListActivityIntent.getKind(intent) + val id = AccountListActivityIntent.getId(intent) setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { - when (type) { - Type.BLOCKS -> setTitle(R.string.title_blocks) - Type.MUTES -> setTitle(R.string.title_mutes) - Type.FOLLOW_REQUESTS -> setTitle(R.string.title_follow_requests) - Type.FOLLOWERS -> setTitle(R.string.title_followers) - Type.FOLLOWS -> setTitle(R.string.title_follows) - Type.REBLOGGED -> setTitle(R.string.title_reblogged_by) - Type.FAVOURITED -> setTitle(R.string.title_favourited_by) + when (kind) { + BLOCKS -> setTitle(R.string.title_blocks) + MUTES -> setTitle(R.string.title_mutes) + FOLLOW_REQUESTS -> setTitle(R.string.title_follow_requests) + FOLLOWERS -> setTitle(R.string.title_followers) + FOLLOWS -> setTitle(R.string.title_follows) + REBLOGGED -> setTitle(R.string.title_reblogged_by) + FAVOURITED -> setTitle(R.string.title_favourited_by) } setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) } supportFragmentManager.commit { - replace(R.id.fragment_container, AccountListFragment.newInstance(type, id)) - } - } - - companion object { - private const val EXTRA_TYPE = "type" - private const val EXTRA_ID = "id" - - fun newIntent(context: Context, type: Type, id: String? = null): Intent { - return Intent(context, AccountListActivity::class.java).apply { - putExtra(EXTRA_TYPE, type) - putExtra(EXTRA_ID, id) - } + replace(R.id.fragment_container, AccountListFragment.newInstance(kind, id)) } } } diff --git a/app/src/main/java/app/pachli/components/accountlist/AccountListFragment.kt b/app/src/main/java/app/pachli/components/accountlist/AccountListFragment.kt index 0df85b0b5..1fb2a7f1f 100644 --- a/app/src/main/java/app/pachli/components/accountlist/AccountListFragment.kt +++ b/app/src/main/java/app/pachli/components/accountlist/AccountListFragment.kt @@ -28,9 +28,6 @@ import app.pachli.BaseActivity import app.pachli.BottomSheetActivity import app.pachli.PostLookupFallbackBehavior import app.pachli.R -import app.pachli.StatusListActivity -import app.pachli.components.account.AccountActivity -import app.pachli.components.accountlist.AccountListActivity.Type import app.pachli.components.accountlist.adapter.AccountAdapter import app.pachli.components.accountlist.adapter.BlocksAdapter import app.pachli.components.accountlist.adapter.FollowAdapter @@ -38,6 +35,16 @@ import app.pachli.components.accountlist.adapter.FollowRequestsAdapter import app.pachli.components.accountlist.adapter.FollowRequestsHeaderAdapter import app.pachli.components.accountlist.adapter.MutesAdapter import app.pachli.core.accounts.AccountManager +import app.pachli.core.navigation.AccountActivityIntent +import app.pachli.core.navigation.AccountListActivityIntent.Kind +import app.pachli.core.navigation.AccountListActivityIntent.Kind.BLOCKS +import app.pachli.core.navigation.AccountListActivityIntent.Kind.FAVOURITED +import app.pachli.core.navigation.AccountListActivityIntent.Kind.FOLLOWERS +import app.pachli.core.navigation.AccountListActivityIntent.Kind.FOLLOWS +import app.pachli.core.navigation.AccountListActivityIntent.Kind.FOLLOW_REQUESTS +import app.pachli.core.navigation.AccountListActivityIntent.Kind.MUTES +import app.pachli.core.navigation.AccountListActivityIntent.Kind.REBLOGGED +import app.pachli.core.navigation.StatusListActivityIntent import app.pachli.core.network.model.HttpHeaderLink import app.pachli.core.network.model.Relationship import app.pachli.core.network.model.TimelineAccount @@ -80,7 +87,7 @@ class AccountListFragment : private val binding by viewBinding(FragmentAccountListBinding::bind) - private lateinit var type: Type + private lateinit var kind: Kind private var id: String? = null private lateinit var scrollListener: EndlessOnScrollListener @@ -90,7 +97,7 @@ class AccountListFragment : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - type = requireArguments().getSerializable(ARG_TYPE) as Type + kind = requireArguments().getSerializable(ARG_KIND) as Kind id = requireArguments().getString(ARG_ID) } @@ -112,10 +119,10 @@ class AccountListFragment : val activeAccount = accountManager.activeAccount!! - adapter = when (type) { - Type.BLOCKS -> BlocksAdapter(this, animateAvatar, animateEmojis, showBotOverlay) - Type.MUTES -> MutesAdapter(this, animateAvatar, animateEmojis, showBotOverlay) - Type.FOLLOW_REQUESTS -> { + adapter = when (kind) { + BLOCKS -> BlocksAdapter(this, animateAvatar, animateEmojis, showBotOverlay) + MUTES -> MutesAdapter(this, animateAvatar, animateEmojis, showBotOverlay) + FOLLOW_REQUESTS -> { val headerAdapter = FollowRequestsHeaderAdapter( instanceName = activeAccount.domain, accountLocked = activeAccount.locked, @@ -151,12 +158,12 @@ class AccountListFragment : override fun onViewTag(tag: String) { (activity as BaseActivity?) - ?.startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag)) + ?.startActivityWithSlideInAnimation(StatusListActivityIntent.hashtag(requireContext(), tag)) } override fun onViewAccount(id: String) { (activity as BaseActivity?)?.let { - val intent = AccountActivity.getIntent(it, id) + val intent = AccountActivityIntent(it, id) it.startActivityWithSlideInAnimation(intent) } } @@ -283,31 +290,31 @@ class AccountListFragment : } private suspend fun getFetchCallByListType(fromId: String?): Response> { - return when (type) { - Type.FOLLOWS -> { - val accountId = requireId(type, id) + return when (kind) { + FOLLOWS -> { + val accountId = requireId(kind, id) api.accountFollowing(accountId, fromId) } - Type.FOLLOWERS -> { - val accountId = requireId(type, id) + FOLLOWERS -> { + val accountId = requireId(kind, id) api.accountFollowers(accountId, fromId) } - Type.BLOCKS -> api.blocks(fromId) - Type.MUTES -> api.mutes(fromId) - Type.FOLLOW_REQUESTS -> api.followRequests(fromId) - Type.REBLOGGED -> { - val statusId = requireId(type, id) + BLOCKS -> api.blocks(fromId) + MUTES -> api.mutes(fromId) + FOLLOW_REQUESTS -> api.followRequests(fromId) + REBLOGGED -> { + val statusId = requireId(kind, id) api.statusRebloggedBy(statusId, fromId) } - Type.FAVOURITED -> { - val statusId = requireId(type, id) + FAVOURITED -> { + val statusId = requireId(kind, id) api.statusFavouritedBy(statusId, fromId) } } } - private fun requireId(type: Type, id: String?): String { - return requireNotNull(id) { "id must not be null for type " + type.name } + private fun requireId(kind: Kind, id: String?): String { + return requireNotNull(id) { "id must not be null for kind " + kind.name } } private fun fetchAccounts(fromId: String? = null) { @@ -410,13 +417,13 @@ class AccountListFragment : } companion object { - private const val ARG_TYPE = "type" + private const val ARG_KIND = "kind" private const val ARG_ID = "id" - fun newInstance(type: Type, id: String? = null): AccountListFragment { + fun newInstance(kind: Kind, id: String? = null): AccountListFragment { return AccountListFragment().apply { arguments = Bundle(3).apply { - putSerializable(ARG_TYPE, type) + putSerializable(ARG_KIND, kind) putString(ARG_ID, id) } } diff --git a/app/src/main/java/app/pachli/components/announcements/AnnouncementsActivity.kt b/app/src/main/java/app/pachli/components/announcements/AnnouncementsActivity.kt index 7c0387535..14b21238e 100644 --- a/app/src/main/java/app/pachli/components/announcements/AnnouncementsActivity.kt +++ b/app/src/main/java/app/pachli/components/announcements/AnnouncementsActivity.kt @@ -16,8 +16,6 @@ package app.pachli.components.announcements -import android.content.Context -import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuInflater @@ -29,9 +27,9 @@ import androidx.core.view.MenuProvider import androidx.recyclerview.widget.LinearLayoutManager import app.pachli.BottomSheetActivity import app.pachli.R -import app.pachli.StatusListActivity import app.pachli.adapter.EmojiAdapter import app.pachli.adapter.OnEmojiSelectedListener +import app.pachli.core.navigation.StatusListActivityIntent import app.pachli.core.preferences.PrefKeys import app.pachli.databinding.ActivityAnnouncementsBinding import app.pachli.util.Error @@ -185,7 +183,7 @@ class AnnouncementsActivity : } override fun onViewTag(tag: String) { - val intent = StatusListActivity.newHashtagIntent(this, tag) + val intent = StatusListActivityIntent.hashtag(this, tag) startActivityWithSlideInAnimation(intent) } @@ -196,8 +194,4 @@ class AnnouncementsActivity : override fun onViewUrl(url: String) { viewUrl(url) } - - companion object { - fun newIntent(context: Context) = Intent(context, AnnouncementsActivity::class.java) - } } diff --git a/app/src/main/java/app/pachli/components/compose/ComposeActivity.kt b/app/src/main/java/app/pachli/components/compose/ComposeActivity.kt index 17d5fda99..87cd79cf4 100644 --- a/app/src/main/java/app/pachli/components/compose/ComposeActivity.kt +++ b/app/src/main/java/app/pachli/components/compose/ComposeActivity.kt @@ -19,7 +19,6 @@ package app.pachli.components.compose import android.Manifest import android.app.ProgressDialog import android.content.ClipData -import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap @@ -28,7 +27,6 @@ import android.graphics.PorterDuffColorFilter import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Parcelable import android.provider.MediaStore import android.text.Spanned import android.text.style.URLSpan @@ -77,10 +75,11 @@ import app.pachli.components.compose.view.ComposeOptionsListener import app.pachli.components.compose.view.ComposeScheduleView import app.pachli.components.instanceinfo.InstanceInfoRepository import app.pachli.core.database.model.AccountEntity -import app.pachli.core.database.model.DraftAttachment +import app.pachli.core.navigation.ComposeActivityIntent +import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions +import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions.InitialCursorPosition import app.pachli.core.network.model.Attachment import app.pachli.core.network.model.Emoji -import app.pachli.core.network.model.NewPoll import app.pachli.core.network.model.Status import app.pachli.core.preferences.PrefKeys import app.pachli.core.preferences.PrefKeys.APP_THEME @@ -116,7 +115,6 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.parcelize.Parcelize import timber.log.Timber import java.io.File import java.io.IOException @@ -125,6 +123,10 @@ import java.util.Locale import kotlin.math.max import kotlin.math.min +/** + * Compose a status, either by creating one from scratch, or by editing an existing + * status, draft, or scheduled status. + */ @AndroidEntryPoint class ComposeActivity : BaseActivity(), @@ -233,7 +235,7 @@ class ComposeActivity : /* If the composer is started up as a reply to another post, override the "starting" state * based on what the intent from the reply request passes. */ - val composeOptions: ComposeOptions? = IntentCompat.getParcelableExtra(intent, COMPOSE_OPTIONS_EXTRA, ComposeOptions::class.java) + val composeOptions: ComposeOptions? = ComposeActivityIntent.getOptions(intent) viewModel.setup(composeOptions) setupButtons() @@ -1297,60 +1299,6 @@ class ComposeActivity : viewModel.updateDescription(localId, description) } - /** - * Status' kind. This particularly affects how the status is handled if the user - * backs out of the edit. - */ - enum class ComposeKind { - /** Status is new */ - NEW, - - /** Editing a posted status */ - EDIT_POSTED, - - /** Editing a status started as an existing draft */ - EDIT_DRAFT, - - /** Editing an an existing scheduled status */ - EDIT_SCHEDULED, - } - - /** - * Initial position of the cursor in EditText when the compose button is clicked - * in a hashtag timeline - */ - enum class InitialCursorPosition { - START, - END, - } - - @Parcelize - data class ComposeOptions( - // Let's keep fields var until all consumers are Kotlin - var scheduledTootId: String? = null, - var draftId: Int? = null, - var content: String? = null, - var mediaUrls: List? = null, - var mediaDescriptions: List? = null, - var mentionedUsernames: Set? = null, - var inReplyToId: String? = null, - var replyVisibility: Status.Visibility? = null, - var visibility: Status.Visibility? = null, - var contentWarning: String? = null, - var replyingStatusAuthor: String? = null, - var replyingStatusContent: String? = null, - var mediaAttachments: List? = null, - var draftAttachments: List? = null, - var scheduledAt: String? = null, - var sensitive: Boolean? = null, - var poll: NewPoll? = null, - var modifiedInitialState: Boolean? = null, - var language: String? = null, - var statusId: String? = null, - var kind: ComposeKind? = null, - var initialCursorPosition: InitialCursorPosition = InitialCursorPosition.END, - ) : Parcelable - companion object { private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 @@ -1360,20 +1308,6 @@ class ComposeActivity : private const val SCHEDULED_TIME_KEY = "SCHEDULE" private const val CONTENT_WARNING_VISIBLE_KEY = "CONTENT_WARNING_VISIBLE" - /** - * @param options ComposeOptions to configure the ComposeActivity - * @return an Intent to start the ComposeActivity - */ - @JvmStatic - fun startIntent( - context: Context, - options: ComposeOptions, - ): Intent { - return Intent(context, ComposeActivity::class.java).apply { - putExtra(COMPOSE_OPTIONS_EXTRA, options) - } - } - fun canHandleMimeType(mimeType: String?): Boolean { return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain") } diff --git a/app/src/main/java/app/pachli/components/compose/ComposeViewModel.kt b/app/src/main/java/app/pachli/components/compose/ComposeViewModel.kt index e34134103..d05a6bdf6 100644 --- a/app/src/main/java/app/pachli/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/app/pachli/components/compose/ComposeViewModel.kt @@ -20,7 +20,6 @@ import android.net.Uri import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.pachli.components.compose.ComposeActivity.ComposeKind import app.pachli.components.compose.ComposeActivity.QueuedMedia import app.pachli.components.compose.ComposeAutoCompleteAdapter.AutocompleteResult import app.pachli.components.drafts.DraftHelper @@ -29,6 +28,8 @@ import app.pachli.components.instanceinfo.InstanceInfoRepository import app.pachli.components.search.SearchType import app.pachli.core.accounts.AccountManager import app.pachli.core.common.string.randomAlphanumericString +import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions +import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions.ComposeKind import app.pachli.core.network.model.Attachment import app.pachli.core.network.model.Emoji import app.pachli.core.network.model.NewPoll @@ -412,7 +413,7 @@ class ComposeViewModel @Inject constructor( } } - fun setup(composeOptions: ComposeActivity.ComposeOptions?) { + fun setup(composeOptions: ComposeOptions?) { if (setupComplete) { return } diff --git a/app/src/main/java/app/pachli/components/conversation/ConversationsFragment.kt b/app/src/main/java/app/pachli/components/conversation/ConversationsFragment.kt index 3c4b4e59a..612471eea 100644 --- a/app/src/main/java/app/pachli/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/app/pachli/components/conversation/ConversationsFragment.kt @@ -36,10 +36,11 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import app.pachli.R -import app.pachli.StatusListActivity import app.pachli.adapter.StatusBaseViewHolder import app.pachli.appstore.EventHub -import app.pachli.components.account.AccountActivity +import app.pachli.core.navigation.AccountActivityIntent +import app.pachli.core.navigation.AttachmentViewData +import app.pachli.core.navigation.StatusListActivityIntent import app.pachli.core.preferences.PrefKeys import app.pachli.core.preferences.SharedPreferencesRepository import app.pachli.databinding.FragmentTimelineBinding @@ -52,7 +53,6 @@ import app.pachli.util.hide import app.pachli.util.show import app.pachli.util.viewBinding import app.pachli.util.visible -import app.pachli.viewdata.AttachmentViewData import at.connyduck.sparkbutton.helpers.Utils import com.google.android.material.color.MaterialColors import com.google.android.material.divider.MaterialDividerItemDecoration @@ -327,12 +327,12 @@ class ConversationsFragment : } override fun onViewAccount(id: String) { - val intent = AccountActivity.getIntent(requireContext(), id) + val intent = AccountActivityIntent(requireContext(), id) startActivity(intent) } override fun onViewTag(tag: String) { - val intent = StatusListActivity.newHashtagIntent(requireContext(), tag) + val intent = StatusListActivityIntent.hashtag(requireContext(), tag) startActivity(intent) } diff --git a/app/src/main/java/app/pachli/components/drafts/DraftsActivity.kt b/app/src/main/java/app/pachli/components/drafts/DraftsActivity.kt index eb3b97540..ff28ae210 100644 --- a/app/src/main/java/app/pachli/components/drafts/DraftsActivity.kt +++ b/app/src/main/java/app/pachli/components/drafts/DraftsActivity.kt @@ -17,7 +17,6 @@ package app.pachli.components.drafts import android.content.Context -import android.content.Intent import android.os.Bundle import android.widget.LinearLayout import android.widget.Toast @@ -26,8 +25,9 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import app.pachli.BaseActivity import app.pachli.R -import app.pachli.components.compose.ComposeActivity import app.pachli.core.database.model.DraftEntity +import app.pachli.core.navigation.ComposeActivityIntent +import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions import app.pachli.core.network.parseAsMastodonHtml import app.pachli.databinding.ActivityDraftsBinding import app.pachli.db.DraftsAlert @@ -106,7 +106,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener { viewModel.getStatus(draft.inReplyToId!!) .fold( { status -> - val composeOptions = ComposeActivity.ComposeOptions( + val composeOptions = ComposeOptions( draftId = draft.id, content = draft.content, contentWarning = draft.contentWarning, @@ -120,12 +120,12 @@ class DraftsActivity : BaseActivity(), DraftActionListener { scheduledAt = draft.scheduledAt, language = draft.language, statusId = draft.statusId, - kind = ComposeActivity.ComposeKind.EDIT_DRAFT, + kind = ComposeOptions.ComposeKind.EDIT_DRAFT, ) bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN - startActivity(ComposeActivity.startIntent(context, composeOptions)) + startActivity(ComposeActivityIntent(context, composeOptions)) }, { throwable -> bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN @@ -147,7 +147,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener { } private fun openDraftWithoutReply(draft: DraftEntity) { - val composeOptions = ComposeActivity.ComposeOptions( + val composeOptions = ComposeOptions( draftId = draft.id, content = draft.content, contentWarning = draft.contentWarning, @@ -158,10 +158,10 @@ class DraftsActivity : BaseActivity(), DraftActionListener { scheduledAt = draft.scheduledAt, language = draft.language, statusId = draft.statusId, - kind = ComposeActivity.ComposeKind.EDIT_DRAFT, + kind = ComposeOptions.ComposeKind.EDIT_DRAFT, ) - startActivity(ComposeActivity.startIntent(this, composeOptions)) + startActivity(ComposeActivityIntent(this, composeOptions)) } override fun onDeleteDraft(draft: DraftEntity) { @@ -172,8 +172,4 @@ class DraftsActivity : BaseActivity(), DraftActionListener { } .show() } - - companion object { - fun newIntent(context: Context) = Intent(context, DraftsActivity::class.java) - } } diff --git a/app/src/main/java/app/pachli/components/filters/EditFilterActivity.kt b/app/src/main/java/app/pachli/components/filters/EditFilterActivity.kt index 3e81c2fc4..1a7493bbd 100644 --- a/app/src/main/java/app/pachli/components/filters/EditFilterActivity.kt +++ b/app/src/main/java/app/pachli/components/filters/EditFilterActivity.kt @@ -8,13 +8,13 @@ import android.widget.AdapterView import android.widget.ArrayAdapter import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog -import androidx.core.content.IntentCompat import androidx.core.view.size import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.lifecycleScope import app.pachli.BaseActivity import app.pachli.R import app.pachli.appstore.EventHub +import app.pachli.core.navigation.EditFilterActivityIntent import app.pachli.core.network.model.Filter import app.pachli.core.network.model.FilterKeyword import app.pachli.core.network.retrofit.MastodonApi @@ -32,6 +32,9 @@ import retrofit2.HttpException import java.util.Date import javax.inject.Inject +/** + * Edit a single server-side filter. + */ @AndroidEntryPoint class EditFilterActivity : BaseActivity() { @Inject @@ -50,7 +53,7 @@ class EditFilterActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - originalFilter = IntentCompat.getParcelableExtra(intent, FILTER_TO_EDIT, Filter::class.java) + originalFilter = EditFilterActivityIntent.getFilter(intent) filter = originalFilter ?: Filter("", "", listOf(), null, Filter.Action.WARN.action, listOf()) binding.apply { contextSwitches = mapOf( @@ -295,8 +298,6 @@ class EditFilterActivity : BaseActivity() { } companion object { - const val FILTER_TO_EDIT = "FilterToEdit" - // Mastodon *stores* the absolute date in the filter, // but create/edit take a number of seconds (relative to the time the operation is posted) fun getSecondsForDurationIndex(index: Int, context: Context?, default: Date? = null): Int? { diff --git a/app/src/main/java/app/pachli/components/filters/FiltersActivity.kt b/app/src/main/java/app/pachli/components/filters/FiltersActivity.kt index 0fb6365c1..7d601c904 100644 --- a/app/src/main/java/app/pachli/components/filters/FiltersActivity.kt +++ b/app/src/main/java/app/pachli/components/filters/FiltersActivity.kt @@ -1,12 +1,12 @@ package app.pachli.components.filters import android.content.DialogInterface.BUTTON_POSITIVE -import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.lifecycle.lifecycleScope import app.pachli.BaseActivity import app.pachli.R +import app.pachli.core.navigation.EditFilterActivityIntent import app.pachli.core.network.model.Filter import app.pachli.databinding.ActivityFiltersBinding import app.pachli.util.hide @@ -95,11 +95,7 @@ class FiltersActivity : BaseActivity(), FiltersListener { } private fun launchEditFilterActivity(filter: Filter? = null) { - val intent = Intent(this, EditFilterActivity::class.java).apply { - if (filter != null) { - putExtra(EditFilterActivity.FILTER_TO_EDIT, filter) - } - } + val intent = EditFilterActivityIntent(this, filter) startActivity(intent) overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) } diff --git a/app/src/main/java/app/pachli/components/login/LoginActivity.kt b/app/src/main/java/app/pachli/components/login/LoginActivity.kt index c38d5462c..13c893c08 100644 --- a/app/src/main/java/app/pachli/components/login/LoginActivity.kt +++ b/app/src/main/java/app/pachli/components/login/LoginActivity.kt @@ -30,8 +30,9 @@ import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope import app.pachli.BaseActivity import app.pachli.BuildConfig -import app.pachli.MainActivity import app.pachli.R +import app.pachli.core.navigation.LoginActivityIntent +import app.pachli.core.navigation.MainActivityIntent import app.pachli.core.network.model.AccessToken import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.preferences.getNonNullString @@ -48,7 +49,11 @@ import okhttp3.HttpUrl import timber.log.Timber import javax.inject.Inject -/** Main login page, the first thing that users see. Has prompt for instance and login button. */ +/** + * Main login page, the first thing that users see. + * + * Has prompt for instance and login button. + */ @AndroidEntryPoint class LoginActivity : BaseActivity() { @@ -318,7 +323,7 @@ class LoginActivity : BaseActivity() { newAccount = newAccount, ) - val intent = Intent(this, MainActivity::class.java) + val intent = MainActivityIntent(this) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK startActivity(intent) finish() @@ -343,33 +348,19 @@ class LoginActivity : BaseActivity() { } private fun isAdditionalLogin(): Boolean { - return intent.getIntExtra(LOGIN_MODE, MODE_DEFAULT) == MODE_ADDITIONAL_LOGIN + return LoginActivityIntent.getLoginMode(intent) == LoginActivityIntent.LoginMode.ADDITIONAL_LOGIN } private fun isAccountMigration(): Boolean { - return intent.getIntExtra(LOGIN_MODE, MODE_DEFAULT) == MODE_MIGRATION + return LoginActivityIntent.getLoginMode(intent) == LoginActivityIntent.LoginMode.MIGRATION } companion object { private const val OAUTH_SCOPES = "read write follow push" - private const val LOGIN_MODE = "LOGIN_MODE" private const val DOMAIN = "domain" private const val CLIENT_ID = "clientId" private const val CLIENT_SECRET = "clientSecret" - const val MODE_DEFAULT = 0 - const val MODE_ADDITIONAL_LOGIN = 1 - - // "Migration" is used to update the OAuth scope granted to the client - const val MODE_MIGRATION = 2 - - @JvmStatic - fun getIntent(context: Context, mode: Int): Intent { - val loginIntent = Intent(context, LoginActivity::class.java) - loginIntent.putExtra(LOGIN_MODE, mode) - return loginIntent - } - /** Make sure the user-entered text is just a fully-qualified domain name. */ private fun canonicalizeDomain(domain: String): String { // Strip any schemes out. diff --git a/app/src/main/java/app/pachli/components/login/LoginWebViewActivity.kt b/app/src/main/java/app/pachli/components/login/LoginWebViewActivity.kt index c50d1d25e..51826581c 100644 --- a/app/src/main/java/app/pachli/components/login/LoginWebViewActivity.kt +++ b/app/src/main/java/app/pachli/components/login/LoginWebViewActivity.kt @@ -39,6 +39,7 @@ import androidx.lifecycle.lifecycleScope import app.pachli.BaseActivity import app.pachli.BuildConfig import app.pachli.R +import app.pachli.core.navigation.LoginWebViewActivityIntent import app.pachli.databinding.ActivityLoginWebviewBinding import app.pachli.util.hide import app.pachli.util.viewBinding @@ -51,7 +52,7 @@ import timber.log.Timber /** Contract for starting [LoginWebViewActivity]. */ class OauthLogin : ActivityResultContract() { override fun createIntent(context: Context, input: LoginData): Intent { - val intent = Intent(context, LoginWebViewActivity::class.java) + val intent = LoginWebViewActivityIntent(context) intent.putExtra(DATA_EXTRA, input) return intent } diff --git a/app/src/main/java/app/pachli/components/notifications/NotificationHelper.kt b/app/src/main/java/app/pachli/components/notifications/NotificationHelper.kt index f1a6f1c24..d797a65f2 100644 --- a/app/src/main/java/app/pachli/components/notifications/NotificationHelper.kt +++ b/app/src/main/java/app/pachli/components/notifications/NotificationHelper.kt @@ -16,6 +16,7 @@ */ package app.pachli.components.notifications +import android.annotation.SuppressLint import android.app.NotificationChannel import android.app.NotificationChannelGroup import android.app.NotificationManager @@ -44,10 +45,10 @@ import app.pachli.MainActivity import app.pachli.MainActivity.Companion.composeIntent import app.pachli.MainActivity.Companion.openNotificationIntent import app.pachli.R -import app.pachli.components.compose.ComposeActivity import app.pachli.core.accounts.AccountManager import app.pachli.core.common.string.unicodeWrap import app.pachli.core.database.model.AccountEntity +import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions import app.pachli.core.network.model.Notification import app.pachli.core.network.parseAsMastodonHtml import app.pachli.receiver.SendStatusBroadcastReceiver @@ -382,6 +383,9 @@ private fun getStatusReplyIntent( } mentionedUsernames.removeAll(setOf(account.username)) mentionedUsernames = ArrayList(LinkedHashSet(mentionedUsernames)) + + // TODO: Revisit suppressing this when this file is moved + @SuppressLint("IntentDetector") val replyIntent = Intent(context, SendStatusBroadcastReceiver::class.java) .setAction(REPLY_ACTION) .putExtra(KEY_SENDER_ACCOUNT_ID, account.id) @@ -417,16 +421,17 @@ private fun getStatusComposeIntent( mentionedUsernames.add(mentionedUsername) } } - val composeOptions = ComposeActivity.ComposeOptions() - composeOptions.inReplyToId = inReplyToId - composeOptions.replyVisibility = replyVisibility - composeOptions.contentWarning = contentWarning - composeOptions.replyingStatusAuthor = citedLocalAuthor - composeOptions.replyingStatusContent = citedText - composeOptions.mentionedUsernames = mentionedUsernames - composeOptions.modifiedInitialState = true - composeOptions.language = language - composeOptions.kind = ComposeActivity.ComposeKind.NEW + val composeOptions = ComposeOptions( + inReplyToId = inReplyToId, + replyVisibility = replyVisibility, + contentWarning = contentWarning, + replyingStatusAuthor = citedLocalAuthor, + replyingStatusContent = citedText, + mentionedUsernames = mentionedUsernames, + modifiedInitialState = true, + language = language, + kind = ComposeOptions.ComposeKind.NEW, + ) val composeIntent = composeIntent(context, composeOptions, account.id, body.id, account.id.toInt()) composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) diff --git a/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt b/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt index fdaa60824..5774e1967 100644 --- a/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt +++ b/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt @@ -46,6 +46,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import app.pachli.R import app.pachli.adapter.StatusBaseViewHolder import app.pachli.components.timeline.TimelineLoadStateAdapter +import app.pachli.core.navigation.AttachmentViewData.Companion.list import app.pachli.core.network.model.Filter import app.pachli.core.network.model.Notification import app.pachli.core.network.model.Status @@ -63,7 +64,6 @@ import app.pachli.util.openLink import app.pachli.util.show import app.pachli.util.viewBinding import app.pachli.util.visible -import app.pachli.viewdata.AttachmentViewData.Companion.list import app.pachli.viewdata.NotificationViewData import at.connyduck.sparkbutton.helpers.Utils import com.google.android.material.color.MaterialColors diff --git a/app/src/main/java/app/pachli/components/notifications/PushNotificationHelper.kt b/app/src/main/java/app/pachli/components/notifications/PushNotificationHelper.kt index 528118d68..cc3674a5d 100644 --- a/app/src/main/java/app/pachli/components/notifications/PushNotificationHelper.kt +++ b/app/src/main/java/app/pachli/components/notifications/PushNotificationHelper.kt @@ -22,9 +22,10 @@ import android.os.Build import android.view.View import androidx.appcompat.app.AlertDialog import app.pachli.R -import app.pachli.components.login.LoginActivity import app.pachli.core.accounts.AccountManager import app.pachli.core.database.model.AccountEntity +import app.pachli.core.navigation.LoginActivityIntent +import app.pachli.core.navigation.LoginActivityIntent.LoginMode import app.pachli.core.network.model.Notification import app.pachli.core.network.retrofit.MastodonApi import app.pachli.core.preferences.SharedPreferencesRepository @@ -78,7 +79,12 @@ private fun showMigrationExplanationDialog( if (currentAccountNeedsMigration(accountManager)) { setMessage(R.string.dialog_push_notification_migration) setPositiveButton(R.string.title_migration_relogin) { _, _ -> - context.startActivity(LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION)) + context.startActivity( + LoginActivityIntent( + context, + LoginMode.MIGRATION, + ), + ) } } else { setMessage(R.string.dialog_push_notification_migration_other_accounts) diff --git a/app/src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt b/app/src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt index e7b31acb5..fd81905c6 100644 --- a/app/src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt +++ b/app/src/main/java/app/pachli/components/preference/AccountPreferencesFragment.kt @@ -24,15 +24,18 @@ import androidx.preference.PreferenceFragmentCompat import app.pachli.BaseActivity import app.pachli.BuildConfig import app.pachli.R -import app.pachli.TabPreferenceActivity import app.pachli.appstore.EventHub -import app.pachli.components.accountlist.AccountListActivity -import app.pachli.components.filters.FiltersActivity -import app.pachli.components.followedtags.FollowedTagsActivity -import app.pachli.components.instancemute.InstanceListActivity -import app.pachli.components.login.LoginActivity import app.pachli.components.notifications.currentAccountNeedsMigration import app.pachli.core.accounts.AccountManager +import app.pachli.core.navigation.AccountListActivityIntent +import app.pachli.core.navigation.FiltersActivityIntent +import app.pachli.core.navigation.FollowedTagsActivityIntent +import app.pachli.core.navigation.InstanceListActivityIntent +import app.pachli.core.navigation.LoginActivityIntent +import app.pachli.core.navigation.LoginActivityIntent.LoginMode +import app.pachli.core.navigation.PreferencesActivityIntent +import app.pachli.core.navigation.PreferencesActivityIntent.PreferenceScreen +import app.pachli.core.navigation.TabPreferenceActivityIntent import app.pachli.core.network.model.Account import app.pachli.core.network.model.Status import app.pachli.core.network.retrofit.MastodonApi @@ -90,7 +93,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { setTitle(R.string.title_tab_preferences) setIcon(R.drawable.ic_tabs) setOnPreferenceClickListener { - val intent = Intent(context, TabPreferenceActivity::class.java) + val intent = TabPreferenceActivityIntent(context) activity?.startActivity(intent) activity?.overridePendingTransition( R.anim.slide_from_right, @@ -104,7 +107,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { setTitle(R.string.title_followed_hashtags) setIcon(R.drawable.ic_hashtag) setOnPreferenceClickListener { - val intent = Intent(context, FollowedTagsActivity::class.java) + val intent = FollowedTagsActivityIntent(context) activity?.startActivity(intent) activity?.overridePendingTransition( R.anim.slide_from_right, @@ -118,8 +121,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { setTitle(R.string.action_view_mutes) setIcon(R.drawable.ic_mute_24dp) setOnPreferenceClickListener { - val intent = Intent(context, AccountListActivity::class.java) - intent.putExtra("type", AccountListActivity.Type.MUTES) + val intent = AccountListActivityIntent(context, AccountListActivityIntent.Kind.MUTES) activity?.startActivity(intent) activity?.overridePendingTransition( R.anim.slide_from_right, @@ -133,8 +135,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { setTitle(R.string.action_view_blocks) icon = makeIcon(GoogleMaterial.Icon.gmd_block) setOnPreferenceClickListener { - val intent = Intent(context, AccountListActivity::class.java) - intent.putExtra("type", AccountListActivity.Type.BLOCKS) + val intent = AccountListActivityIntent(context, AccountListActivityIntent.Kind.BLOCKS) activity?.startActivity(intent) activity?.overridePendingTransition( R.anim.slide_from_right, @@ -148,7 +149,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { setTitle(R.string.title_domain_mutes) setIcon(R.drawable.ic_mute_24dp) setOnPreferenceClickListener { - val intent = Intent(context, InstanceListActivity::class.java) + val intent = InstanceListActivityIntent(context) activity?.startActivity(intent) activity?.overridePendingTransition( R.anim.slide_from_right, @@ -163,7 +164,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { setTitle(R.string.title_migration_relogin) setIcon(R.drawable.ic_logout) setOnPreferenceClickListener { - val intent = LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION) + val intent = LoginActivityIntent(context, LoginMode.MIGRATION) (activity as BaseActivity).startActivityWithSlideInAnimation(intent) true } @@ -280,7 +281,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { } else { activity?.let { val intent = - PreferencesActivity.newIntent(it, PreferencesActivity.NOTIFICATION_PREFERENCES) + PreferencesActivityIntent(it, PreferenceScreen.NOTIFICATION) it.startActivity(intent) it.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) } @@ -346,7 +347,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat() { } private fun launchFilterActivity() { - val intent = Intent(context, FiltersActivity::class.java) + val intent = FiltersActivityIntent(requireContext()) activity?.startActivity(intent) activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) } diff --git a/app/src/main/java/app/pachli/components/preference/PreferencesActivity.kt b/app/src/main/java/app/pachli/components/preference/PreferencesActivity.kt index af44b01a0..c73808f70 100644 --- a/app/src/main/java/app/pachli/components/preference/PreferencesActivity.kt +++ b/app/src/main/java/app/pachli/components/preference/PreferencesActivity.kt @@ -16,7 +16,6 @@ package app.pachli.components.preference -import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.OnBackPressedCallback @@ -26,9 +25,11 @@ import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import app.pachli.BaseActivity -import app.pachli.MainActivity import app.pachli.R import app.pachli.appstore.EventHub +import app.pachli.core.navigation.MainActivityIntent +import app.pachli.core.navigation.PreferencesActivityIntent +import app.pachli.core.navigation.PreferencesActivityIntent.PreferenceScreen import app.pachli.core.preferences.PrefKeys import app.pachli.core.preferences.PrefKeys.APP_THEME import app.pachli.core.preferences.getNonNullString @@ -41,6 +42,9 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +/** + * Show specific preferences. + */ @AndroidEntryPoint class PreferencesActivity : BaseActivity(), @@ -55,7 +59,7 @@ class PreferencesActivity : * Either the back stack activities need to all be recreated, or do the easier thing, which * is hijack the back button press and use it to launch a new MainActivity and clear the * back stack. */ - val intent = Intent(this@PreferencesActivity, MainActivity::class.java) + val intent = MainActivityIntent(this@PreferencesActivity) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK startActivityWithSlideInAnimation(intent) } @@ -73,15 +77,15 @@ class PreferencesActivity : setDisplayShowHomeEnabled(true) } - val preferenceType = intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0) + val preferenceType = PreferencesActivityIntent.getPreferenceType(intent) val fragmentTag = "preference_fragment_$preferenceType" val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag) ?: when (preferenceType) { - GENERAL_PREFERENCES -> PreferencesFragment.newInstance() - ACCOUNT_PREFERENCES -> AccountPreferencesFragment.newInstance() - NOTIFICATION_PREFERENCES -> NotificationPreferencesFragment.newInstance() + PreferenceScreen.GENERAL -> PreferencesFragment.newInstance() + PreferenceScreen.ACCOUNT -> AccountPreferencesFragment.newInstance() + PreferenceScreen.NOTIFICATION -> NotificationPreferencesFragment.newInstance() else -> throw IllegalArgumentException("preferenceType not known") } @@ -164,17 +168,6 @@ class PreferencesActivity : } companion object { - const val GENERAL_PREFERENCES = 0 - const val ACCOUNT_PREFERENCES = 1 - const val NOTIFICATION_PREFERENCES = 2 - private const val EXTRA_PREFERENCE_TYPE = "EXTRA_PREFERENCE_TYPE" private const val EXTRA_RESTART_ON_BACK = "restart" - - @JvmStatic - fun newIntent(context: Context, preferenceType: Int): Intent { - val intent = Intent(context, PreferencesActivity::class.java) - intent.putExtra(EXTRA_PREFERENCE_TYPE, preferenceType) - return intent - } } } diff --git a/app/src/main/java/app/pachli/components/report/ReportActivity.kt b/app/src/main/java/app/pachli/components/report/ReportActivity.kt index f3f3bf6b4..1f9360ed8 100644 --- a/app/src/main/java/app/pachli/components/report/ReportActivity.kt +++ b/app/src/main/java/app/pachli/components/report/ReportActivity.kt @@ -16,17 +16,19 @@ package app.pachli.components.report -import android.content.Context -import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import app.pachli.BottomSheetActivity import app.pachli.R import app.pachli.components.report.adapter.ReportPagerAdapter +import app.pachli.core.navigation.ReportActivityIntent import app.pachli.databinding.ActivityReportBinding import app.pachli.util.viewBinding import dagger.hilt.android.AndroidEntryPoint +/** + * Report a status or user. + */ @AndroidEntryPoint class ReportActivity : BottomSheetActivity() { private val viewModel: ReportViewModel by viewModels() @@ -35,13 +37,13 @@ class ReportActivity : BottomSheetActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val accountId = intent?.getStringExtra(ACCOUNT_ID) - val accountUserName = intent?.getStringExtra(ACCOUNT_USERNAME) - if (accountId.isNullOrBlank() || accountUserName.isNullOrBlank()) { - throw IllegalStateException("accountId ($accountId) or accountUserName ($accountUserName) is null") + val accountId = ReportActivityIntent.getAccountId(intent) + val accountUserName = ReportActivityIntent.getAccountUserName(intent) + if (accountId.isBlank() || accountUserName.isBlank()) { + throw IllegalStateException("accountId ($accountId) or accountUserName ($accountUserName) is blank") } - viewModel.init(accountId, accountUserName, intent?.getStringExtra(STATUS_ID)) + viewModel.init(accountId, accountUserName, ReportActivityIntent.getStatusId(intent)) setContentView(binding.root) @@ -115,19 +117,4 @@ class ReportActivity : BottomSheetActivity() { private fun showStatusesPage() { binding.wizard.currentItem = 0 } - - companion object { - private const val ACCOUNT_ID = "account_id" - private const val ACCOUNT_USERNAME = "account_username" - private const val STATUS_ID = "status_id" - - @JvmStatic - fun getIntent(context: Context, accountId: String, userName: String, statusId: String? = null) = - Intent(context, ReportActivity::class.java) - .apply { - putExtra(ACCOUNT_ID, accountId) - putExtra(ACCOUNT_USERNAME, userName) - putExtra(STATUS_ID, statusId) - } - } } diff --git a/app/src/main/java/app/pachli/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/app/pachli/components/report/fragments/ReportStatusesFragment.kt index 4c5f708d9..a0bfd98e9 100644 --- a/app/src/main/java/app/pachli/components/report/fragments/ReportStatusesFragment.kt +++ b/app/src/main/java/app/pachli/components/report/fragments/ReportStatusesFragment.kt @@ -33,20 +33,20 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.SimpleItemAnimator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import app.pachli.R -import app.pachli.StatusListActivity -import app.pachli.ViewMediaActivity -import app.pachli.components.account.AccountActivity import app.pachli.components.report.ReportViewModel import app.pachli.components.report.Screen import app.pachli.components.report.adapter.AdapterHandler import app.pachli.components.report.adapter.StatusesAdapter import app.pachli.core.accounts.AccountManager +import app.pachli.core.navigation.AccountActivityIntent +import app.pachli.core.navigation.AttachmentViewData +import app.pachli.core.navigation.StatusListActivityIntent +import app.pachli.core.navigation.ViewMediaActivityIntent import app.pachli.core.network.model.Attachment import app.pachli.core.network.model.Status import app.pachli.databinding.FragmentReportStatusesBinding import app.pachli.util.viewBinding import app.pachli.util.visible -import app.pachli.viewdata.AttachmentViewData import com.google.android.material.color.MaterialColors import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.snackbar.Snackbar @@ -82,7 +82,7 @@ class ReportStatusesFragment : when (actionable.attachments[idx].type) { Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { val attachments = AttachmentViewData.list(actionable) - val intent = ViewMediaActivity.newIntent(context, attachments, idx) + val intent = ViewMediaActivityIntent(requireContext(), attachments, idx) if (v != null) { val url = actionable.attachments[idx].url ViewCompat.setTransitionName(v, url) @@ -209,9 +209,9 @@ class ReportStatusesFragment : return viewModel.isStatusChecked(id) } - override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id)) + override fun onViewAccount(id: String) = startActivity(AccountActivityIntent(requireContext(), id)) - override fun onViewTag(tag: String) = startActivity(StatusListActivity.newHashtagIntent(requireContext(), tag)) + override fun onViewTag(tag: String) = startActivity(StatusListActivityIntent.hashtag(requireContext(), tag)) override fun onViewUrl(url: String) = viewModel.checkClickedUrl(url) diff --git a/app/src/main/java/app/pachli/components/scheduled/ScheduledStatusActivity.kt b/app/src/main/java/app/pachli/components/scheduled/ScheduledStatusActivity.kt index 8f4a15a71..a03fac186 100644 --- a/app/src/main/java/app/pachli/components/scheduled/ScheduledStatusActivity.kt +++ b/app/src/main/java/app/pachli/components/scheduled/ScheduledStatusActivity.kt @@ -16,8 +16,6 @@ package app.pachli.components.scheduled -import android.content.Context -import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuInflater @@ -32,7 +30,8 @@ import app.pachli.BaseActivity import app.pachli.R import app.pachli.appstore.EventHub import app.pachli.appstore.StatusScheduledEvent -import app.pachli.components.compose.ComposeActivity +import app.pachli.core.navigation.ComposeActivityIntent +import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions import app.pachli.core.network.model.ScheduledStatus import app.pachli.databinding.ActivityScheduledStatusBinding import app.pachli.util.hide @@ -151,9 +150,9 @@ class ScheduledStatusActivity : } override fun edit(item: ScheduledStatus) { - val intent = ComposeActivity.startIntent( + val intent = ComposeActivityIntent( this, - ComposeActivity.ComposeOptions( + ComposeOptions( scheduledTootId = item.id, content = item.params.text, contentWarning = item.params.spoilerText, @@ -162,7 +161,7 @@ class ScheduledStatusActivity : visibility = item.params.visibility, scheduledAt = item.scheduledAt, sensitive = item.params.sensitive, - kind = ComposeActivity.ComposeKind.EDIT_SCHEDULED, + kind = ComposeOptions.ComposeKind.EDIT_SCHEDULED, ), ) startActivity(intent) @@ -177,8 +176,4 @@ class ScheduledStatusActivity : } .show() } - - companion object { - fun newIntent(context: Context) = Intent(context, ScheduledStatusActivity::class.java) - } } diff --git a/app/src/main/java/app/pachli/components/search/SearchActivity.kt b/app/src/main/java/app/pachli/components/search/SearchActivity.kt index c94699e93..e953c6046 100644 --- a/app/src/main/java/app/pachli/components/search/SearchActivity.kt +++ b/app/src/main/java/app/pachli/components/search/SearchActivity.kt @@ -153,8 +153,4 @@ class SearchActivity : BottomSheetActivity(), MenuProvider, SearchView.OnQueryTe return false } - - companion object { - fun getIntent(context: Context) = Intent(context, SearchActivity::class.java) - } } diff --git a/app/src/main/java/app/pachli/components/search/fragments/SearchFragment.kt b/app/src/main/java/app/pachli/components/search/fragments/SearchFragment.kt index b295ebe82..0a1252a38 100644 --- a/app/src/main/java/app/pachli/components/search/fragments/SearchFragment.kt +++ b/app/src/main/java/app/pachli/components/search/fragments/SearchFragment.kt @@ -18,9 +18,9 @@ import androidx.recyclerview.widget.SimpleItemAnimator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import app.pachli.BottomSheetActivity import app.pachli.R -import app.pachli.StatusListActivity -import app.pachli.components.account.AccountActivity import app.pachli.components.search.SearchViewModel +import app.pachli.core.navigation.AccountActivityIntent +import app.pachli.core.navigation.StatusListActivityIntent import app.pachli.core.network.retrofit.MastodonApi import app.pachli.databinding.FragmentSearchBinding import app.pachli.interfaces.LinkListener @@ -141,11 +141,11 @@ abstract class SearchFragment : } override fun onViewAccount(id: String) { - bottomSheetActivity?.startActivityWithSlideInAnimation(AccountActivity.getIntent(requireContext(), id)) + bottomSheetActivity?.startActivityWithSlideInAnimation(AccountActivityIntent(requireContext(), id)) } override fun onViewTag(tag: String) { - bottomSheetActivity?.startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag)) + bottomSheetActivity?.startActivityWithSlideInAnimation(StatusListActivityIntent.hashtag(requireContext(), tag)) } override fun onViewUrl(url: String) { diff --git a/app/src/main/java/app/pachli/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/app/pachli/components/search/fragments/SearchStatusesFragment.kt index 1168f34bf..5e613aaa2 100644 --- a/app/src/main/java/app/pachli/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/app/pachli/components/search/fragments/SearchStatusesFragment.kt @@ -38,12 +38,13 @@ import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.LinearLayoutManager import app.pachli.BaseActivity import app.pachli.R -import app.pachli.ViewMediaActivity -import app.pachli.components.compose.ComposeActivity -import app.pachli.components.compose.ComposeActivity.ComposeOptions -import app.pachli.components.report.ReportActivity import app.pachli.components.search.adapter.SearchStatusesAdapter import app.pachli.core.database.model.AccountEntity +import app.pachli.core.navigation.AttachmentViewData +import app.pachli.core.navigation.ComposeActivityIntent +import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions +import app.pachli.core.navigation.ReportActivityIntent +import app.pachli.core.navigation.ViewMediaActivityIntent import app.pachli.core.network.model.Attachment import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status.Mention @@ -52,7 +53,6 @@ import app.pachli.interfaces.StatusActionListener import app.pachli.util.StatusDisplayOptionsRepository import app.pachli.util.openLink import app.pachli.view.showMuteAccountDialog -import app.pachli.viewdata.AttachmentViewData import app.pachli.viewdata.StatusViewData import at.connyduck.calladapter.networkresult.fold import com.google.android.material.divider.MaterialDividerItemDecoration @@ -119,8 +119,8 @@ class SearchStatusesFragment : SearchFragment(), StatusActionLis when (actionable.attachments[attachmentIndex].type) { Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { val attachments = AttachmentViewData.list(actionable) - val intent = ViewMediaActivity.newIntent( - context, + val intent = ViewMediaActivityIntent( + requireContext(), attachments, attachmentIndex, ) @@ -202,7 +202,7 @@ class SearchStatusesFragment : SearchFragment(), StatusActionLis remove(viewModel.activeAccount?.username) } - val intent = ComposeActivity.startIntent( + val intent = ComposeActivityIntent( requireContext(), ComposeOptions( inReplyToId = status.actionableId, @@ -212,7 +212,7 @@ class SearchStatusesFragment : SearchFragment(), StatusActionLis replyingStatusAuthor = actionableStatus.account.localUsername, replyingStatusContent = status.content.toString(), language = actionableStatus.language, - kind = ComposeActivity.ComposeKind.NEW, + kind = ComposeOptions.ComposeKind.NEW, ), ) bottomSheetActivity?.startActivityWithSlideInAnimation(intent) @@ -423,7 +423,7 @@ class SearchStatusesFragment : SearchFragment(), StatusActionLis } private fun openReportPage(accountId: String, accountUsername: String, statusId: String) { - startActivity(ReportActivity.getIntent(requireContext(), accountId, accountUsername, statusId)) + startActivity(ReportActivityIntent(requireContext(), accountId, accountUsername, statusId)) } private fun showConfirmDeleteDialog(id: String, position: Int) { @@ -455,7 +455,7 @@ class SearchStatusesFragment : SearchFragment(), StatusActionLis deletedStatus } - val intent = ComposeActivity.startIntent( + val intent = ComposeActivityIntent( requireContext(), ComposeOptions( content = redraftStatus.text.orEmpty(), @@ -466,7 +466,7 @@ class SearchStatusesFragment : SearchFragment(), StatusActionLis sensitive = redraftStatus.sensitive, poll = redraftStatus.poll?.toNewPoll(status.createdAt), language = redraftStatus.language, - kind = ComposeActivity.ComposeKind.NEW, + kind = ComposeOptions.ComposeKind.NEW, ), ) startActivity(intent) @@ -497,9 +497,9 @@ class SearchStatusesFragment : SearchFragment(), StatusActionLis language = status.language, statusId = source.id, poll = status.poll?.toNewPoll(status.createdAt), - kind = ComposeActivity.ComposeKind.EDIT_POSTED, + kind = ComposeOptions.ComposeKind.EDIT_POSTED, ) - startActivity(ComposeActivity.startIntent(requireContext(), composeOptions)) + startActivity(ComposeActivityIntent(requireContext(), composeOptions)) }, { Snackbar.make( diff --git a/app/src/main/java/app/pachli/components/timeline/TimelineFragment.kt b/app/src/main/java/app/pachli/components/timeline/TimelineFragment.kt index b85099a06..cd2ceab92 100644 --- a/app/src/main/java/app/pachli/components/timeline/TimelineFragment.kt +++ b/app/src/main/java/app/pachli/components/timeline/TimelineFragment.kt @@ -42,8 +42,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import app.pachli.BaseActivity import app.pachli.R import app.pachli.adapter.StatusBaseViewHolder -import app.pachli.components.accountlist.AccountListActivity -import app.pachli.components.accountlist.AccountListActivity.Companion.newIntent import app.pachli.components.timeline.viewmodel.CachedTimelineViewModel import app.pachli.components.timeline.viewmodel.InfallibleUiAction import app.pachli.components.timeline.viewmodel.NetworkTimelineViewModel @@ -52,6 +50,8 @@ import app.pachli.components.timeline.viewmodel.StatusActionSuccess import app.pachli.components.timeline.viewmodel.TimelineViewModel import app.pachli.components.timeline.viewmodel.UiSuccess import app.pachli.core.database.model.TranslationState +import app.pachli.core.navigation.AccountListActivityIntent +import app.pachli.core.navigation.AttachmentViewData import app.pachli.core.network.model.Status import app.pachli.core.network.model.TimelineKind import app.pachli.databinding.FragmentTimelineBinding @@ -72,7 +72,6 @@ import app.pachli.util.show import app.pachli.util.viewBinding import app.pachli.util.visible import app.pachli.util.withPresentationState -import app.pachli.viewdata.AttachmentViewData import app.pachli.viewdata.StatusViewData import at.connyduck.sparkbutton.helpers.Utils import com.google.android.material.color.MaterialColors @@ -635,13 +634,13 @@ class TimelineFragment : override fun onShowReblogs(position: Int) { val statusId = adapter.peek(position)?.id ?: return - val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId) + val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.REBLOGGED, statusId) (activity as BaseActivity).startActivityWithSlideInAnimation(intent) } override fun onShowFavs(position: Int) { val statusId = adapter.peek(position)?.id ?: return - val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId) + val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.FAVOURITED, statusId) (activity as BaseActivity).startActivityWithSlideInAnimation(intent) } diff --git a/app/src/main/java/app/pachli/components/trending/TrendingActivity.kt b/app/src/main/java/app/pachli/components/trending/TrendingActivity.kt index a246bd6a1..c5220ed8b 100644 --- a/app/src/main/java/app/pachli/components/trending/TrendingActivity.kt +++ b/app/src/main/java/app/pachli/components/trending/TrendingActivity.kt @@ -18,8 +18,6 @@ package app.pachli.components.trending import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuInflater @@ -86,10 +84,6 @@ class TrendingActivity : BottomSheetActivity(), AppBarLayoutHost, MenuProvider { override fun onMenuItemSelected(menuItem: MenuItem): Boolean { return super.onOptionsItemSelected(menuItem) } - - companion object { - fun getIntent(context: Context) = Intent(context, TrendingActivity::class.java) - } } class TrendingFragmentAdapter(val activity: FragmentActivity) : FragmentStateAdapter(activity) { diff --git a/app/src/main/java/app/pachli/components/trending/TrendingTagsFragment.kt b/app/src/main/java/app/pachli/components/trending/TrendingTagsFragment.kt index 06fb52e32..00178d351 100644 --- a/app/src/main/java/app/pachli/components/trending/TrendingTagsFragment.kt +++ b/app/src/main/java/app/pachli/components/trending/TrendingTagsFragment.kt @@ -38,8 +38,8 @@ import androidx.recyclerview.widget.SimpleItemAnimator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import app.pachli.BaseActivity import app.pachli.R -import app.pachli.StatusListActivity import app.pachli.components.trending.viewmodel.TrendingTagsViewModel +import app.pachli.core.navigation.StatusListActivityIntent import app.pachli.databinding.FragmentTrendingTagsBinding import app.pachli.interfaces.ActionButtonActivity import app.pachli.interfaces.AppBarLayoutHost @@ -173,7 +173,7 @@ class TrendingTagsFragment : fun onViewTag(tag: String) { (requireActivity() as BaseActivity).startActivityWithSlideInAnimation( - StatusListActivity.newHashtagIntent( + StatusListActivityIntent.hashtag( requireContext(), tag, ), diff --git a/app/src/main/java/app/pachli/components/viewthread/ViewThreadActivity.kt b/app/src/main/java/app/pachli/components/viewthread/ViewThreadActivity.kt index 4055af7f5..484044f92 100644 --- a/app/src/main/java/app/pachli/components/viewthread/ViewThreadActivity.kt +++ b/app/src/main/java/app/pachli/components/viewthread/ViewThreadActivity.kt @@ -16,16 +16,18 @@ package app.pachli.components.viewthread -import android.content.Context -import android.content.Intent import android.os.Bundle import androidx.fragment.app.commit import app.pachli.BottomSheetActivity import app.pachli.R +import app.pachli.core.navigation.ViewThreadActivityIntent import app.pachli.databinding.ActivityViewThreadBinding import app.pachli.util.viewBinding import dagger.hilt.android.AndroidEntryPoint +/** + * View the statuses in a single thread. + */ @AndroidEntryPoint class ViewThreadActivity : BottomSheetActivity() { private val binding by viewBinding(ActivityViewThreadBinding::inflate) @@ -39,8 +41,8 @@ class ViewThreadActivity : BottomSheetActivity() { setDisplayShowHomeEnabled(true) setDisplayShowTitleEnabled(true) } - val id = intent.getStringExtra(ID_EXTRA)!! - val url = intent.getStringExtra(URL_EXTRA)!! + val id = ViewThreadActivityIntent.getStatusId(intent) + val url = ViewThreadActivityIntent.getUrl(intent) val fragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG + id) as ViewThreadFragment? ?: ViewThreadFragment.newInstance(id, url) @@ -51,15 +53,6 @@ class ViewThreadActivity : BottomSheetActivity() { } companion object { - fun startIntent(context: Context, id: String, url: String): Intent { - val intent = Intent(context, ViewThreadActivity::class.java) - intent.putExtra(ID_EXTRA, id) - intent.putExtra(URL_EXTRA, url) - return intent - } - - private const val ID_EXTRA = "id" - private const val URL_EXTRA = "url" private const val FRAGMENT_TAG = "ViewThreadFragment_" } } diff --git a/app/src/main/java/app/pachli/components/viewthread/ViewThreadFragment.kt b/app/src/main/java/app/pachli/components/viewthread/ViewThreadFragment.kt index 8aaaf9401..a54c29a25 100644 --- a/app/src/main/java/app/pachli/components/viewthread/ViewThreadFragment.kt +++ b/app/src/main/java/app/pachli/components/viewthread/ViewThreadFragment.kt @@ -34,9 +34,9 @@ import androidx.recyclerview.widget.SimpleItemAnimator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import app.pachli.BaseActivity import app.pachli.R -import app.pachli.components.accountlist.AccountListActivity -import app.pachli.components.accountlist.AccountListActivity.Companion.newIntent import app.pachli.components.viewthread.edits.ViewEditsFragment +import app.pachli.core.navigation.AccountListActivityIntent +import app.pachli.core.navigation.AttachmentViewData.Companion.list import app.pachli.databinding.FragmentViewThreadBinding import app.pachli.fragment.SFragment import app.pachli.interfaces.StatusActionListener @@ -45,7 +45,6 @@ import app.pachli.util.hide import app.pachli.util.openLink import app.pachli.util.show import app.pachli.util.viewBinding -import app.pachli.viewdata.AttachmentViewData.Companion.list import app.pachli.viewdata.StatusViewData import com.google.android.material.color.MaterialColors import com.google.android.material.divider.MaterialDividerItemDecoration @@ -366,13 +365,13 @@ class ViewThreadFragment : override fun onShowReblogs(position: Int) { val statusId = adapter.currentList[position].id - val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId) + val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.REBLOGGED, statusId) (requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent) } override fun onShowFavs(position: Int) { val statusId = adapter.currentList[position].id - val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId) + val intent = AccountListActivityIntent(requireContext(), AccountListActivityIntent.Kind.FAVOURITED, statusId) (requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent) } @@ -423,7 +422,7 @@ class ViewThreadFragment : private const val ID_EXTRA = "id" private const val URL_EXTRA = "url" - fun newInstance(id: String, url: String): ViewThreadFragment { + fun newInstance(id: String, url: String?): ViewThreadFragment { val arguments = Bundle(2) val fragment = ViewThreadFragment() arguments.putString(ID_EXTRA, id) diff --git a/app/src/main/java/app/pachli/components/viewthread/edits/ViewEditsFragment.kt b/app/src/main/java/app/pachli/components/viewthread/edits/ViewEditsFragment.kt index 01baf3b24..72788c5d6 100644 --- a/app/src/main/java/app/pachli/components/viewthread/edits/ViewEditsFragment.kt +++ b/app/src/main/java/app/pachli/components/viewthread/edits/ViewEditsFragment.kt @@ -31,9 +31,9 @@ import androidx.recyclerview.widget.SimpleItemAnimator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import app.pachli.BottomSheetActivity import app.pachli.R -import app.pachli.StatusListActivity -import app.pachli.components.account.AccountActivity import app.pachli.core.common.string.unicodeWrap +import app.pachli.core.navigation.AccountActivityIntent +import app.pachli.core.navigation.StatusListActivityIntent import app.pachli.core.preferences.PrefKeys import app.pachli.core.preferences.SharedPreferencesRepository import app.pachli.databinding.FragmentViewEditsBinding @@ -184,11 +184,11 @@ class ViewEditsFragment : } override fun onViewAccount(id: String) { - bottomSheetActivity?.startActivityWithSlideInAnimation(AccountActivity.getIntent(requireContext(), id)) + bottomSheetActivity?.startActivityWithSlideInAnimation(AccountActivityIntent(requireContext(), id)) } override fun onViewTag(tag: String) { - bottomSheetActivity?.startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag)) + bottomSheetActivity?.startActivityWithSlideInAnimation(StatusListActivityIntent.hashtag(requireContext(), tag)) } override fun onViewUrl(url: String) { diff --git a/app/src/main/java/app/pachli/db/DraftsAlert.kt b/app/src/main/java/app/pachli/db/DraftsAlert.kt index 960653760..c78c61318 100644 --- a/app/src/main/java/app/pachli/db/DraftsAlert.kt +++ b/app/src/main/java/app/pachli/db/DraftsAlert.kt @@ -23,9 +23,9 @@ import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import app.pachli.R -import app.pachli.components.drafts.DraftsActivity import app.pachli.core.accounts.AccountManager import app.pachli.core.database.dao.DraftDao +import app.pachli.core.navigation.DraftsActivityIntent import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -66,7 +66,7 @@ class DraftsAlert @Inject constructor(private val draftDao: DraftDao) { .setPositiveButton(R.string.action_post_failed_show_drafts) { _: DialogInterface?, _: Int -> clearDraftsAlert(coroutineScope, activeAccountId) // User looked at drafts - val intent = DraftsActivity.newIntent(context) + val intent = DraftsActivityIntent(context) context.startActivity(intent) } .setNegativeButton(R.string.action_post_failed_do_nothing) { _: DialogInterface?, _: Int -> diff --git a/app/src/main/java/app/pachli/fragment/SFragment.kt b/app/src/main/java/app/pachli/fragment/SFragment.kt index 499faf860..9e1b5ae0b 100644 --- a/app/src/main/java/app/pachli/fragment/SFragment.kt +++ b/app/src/main/java/app/pachli/fragment/SFragment.kt @@ -42,15 +42,15 @@ import app.pachli.BaseActivity import app.pachli.BottomSheetActivity import app.pachli.PostLookupFallbackBehavior import app.pachli.R -import app.pachli.StatusListActivity.Companion.newHashtagIntent -import app.pachli.ViewMediaActivity.Companion.newIntent -import app.pachli.components.compose.ComposeActivity -import app.pachli.components.compose.ComposeActivity.Companion.startIntent -import app.pachli.components.compose.ComposeActivity.ComposeOptions -import app.pachli.components.report.ReportActivity.Companion.getIntent import app.pachli.core.accounts.AccountManager import app.pachli.core.database.model.AccountEntity import app.pachli.core.database.model.TranslationState +import app.pachli.core.navigation.AttachmentViewData +import app.pachli.core.navigation.ComposeActivityIntent +import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions +import app.pachli.core.navigation.ReportActivityIntent +import app.pachli.core.navigation.StatusListActivityIntent +import app.pachli.core.navigation.ViewMediaActivityIntent import app.pachli.core.network.ServerOperation import app.pachli.core.network.model.Attachment import app.pachli.core.network.model.Status @@ -61,7 +61,6 @@ import app.pachli.network.ServerCapabilitiesRepository import app.pachli.usecase.TimelineCases import app.pachli.util.openLink import app.pachli.view.showMuteAccountDialog -import app.pachli.viewdata.AttachmentViewData import app.pachli.viewdata.StatusViewData import at.connyduck.calladapter.networkresult.fold import at.connyduck.calladapter.networkresult.onFailure @@ -163,10 +162,10 @@ abstract class SFragment : Fragment() { replyingStatusAuthor = account.localUsername, replyingStatusContent = actionableStatus.content.parseAsMastodonHtml().toString(), language = actionableStatus.language, - kind = ComposeActivity.ComposeKind.NEW, + kind = ComposeOptions.ComposeKind.NEW, ) - val intent = startIntent(requireContext(), composeOptions) + val intent = ComposeActivityIntent(requireContext(), composeOptions) requireActivity().startActivity(intent) } @@ -379,7 +378,7 @@ abstract class SFragment : Fragment() { val (attachment) = attachments[urlIndex] when (attachment.type) { Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { - val intent = newIntent(context, attachments, urlIndex) + val intent = ViewMediaActivityIntent(requireContext(), attachments, urlIndex) if (view != null) { val url = attachment.url view.transitionName = url @@ -400,11 +399,11 @@ abstract class SFragment : Fragment() { } protected fun viewTag(tag: String) { - startActivity(newHashtagIntent(requireContext(), tag)) + startActivity(StatusListActivityIntent.hashtag(requireContext(), tag)) } private fun openReportPage(accountId: String, accountUsername: String, statusId: String) { - startActivity(getIntent(requireContext(), accountId, accountUsername, statusId)) + startActivity(ReportActivityIntent(requireContext(), accountId, accountUsername, statusId)) } private fun showConfirmDeleteDialog(id: String, position: Int) { @@ -455,9 +454,9 @@ abstract class SFragment : Fragment() { modifiedInitialState = true, language = sourceStatus.language, poll = sourceStatus.poll?.toNewPoll(sourceStatus.createdAt), - kind = ComposeActivity.ComposeKind.NEW, + kind = ComposeOptions.ComposeKind.NEW, ) - startActivity(startIntent(requireContext(), composeOptions)) + startActivity(ComposeActivityIntent(requireContext(), composeOptions)) }, { error: Throwable? -> Timber.w(error, "error deleting status") @@ -485,9 +484,9 @@ abstract class SFragment : Fragment() { language = status.language, statusId = source.id, poll = status.poll?.toNewPoll(status.createdAt), - kind = ComposeActivity.ComposeKind.EDIT_POSTED, + kind = ComposeOptions.ComposeKind.EDIT_POSTED, ) - startActivity(startIntent(requireContext(), composeOptions)) + startActivity(ComposeActivityIntent(requireContext(), composeOptions)) }, { Snackbar.make( diff --git a/app/src/main/java/app/pachli/service/PachliTileService.kt b/app/src/main/java/app/pachli/service/PachliTileService.kt index 942a990de..7c2a41d30 100644 --- a/app/src/main/java/app/pachli/service/PachliTileService.kt +++ b/app/src/main/java/app/pachli/service/PachliTileService.kt @@ -24,8 +24,8 @@ import android.content.Intent import android.os.Build import android.service.quicksettings.TileService import app.pachli.MainActivity -import app.pachli.components.compose.ComposeActivity import app.pachli.components.notifications.pendingIntentFlags +import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions /** * Small Addition that adds in a QuickSettings tile @@ -35,7 +35,7 @@ import app.pachli.components.notifications.pendingIntentFlags class PachliTileService : TileService() { @SuppressLint("StartActivityAndCollapseDeprecated") override fun onClick() { - val intent = MainActivity.composeIntent(this, ComposeActivity.ComposeOptions()) + val intent = MainActivity.composeIntent(this, ComposeOptions()) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { startActivityAndCollapse(getActivityPendingIntent(this, 0, intent)) diff --git a/app/src/main/java/app/pachli/service/SendStatusService.kt b/app/src/main/java/app/pachli/service/SendStatusService.kt index 300243d3e..2bcaabbcf 100644 --- a/app/src/main/java/app/pachli/service/SendStatusService.kt +++ b/app/src/main/java/app/pachli/service/SendStatusService.kt @@ -1,5 +1,6 @@ package app.pachli.service +import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager @@ -373,6 +374,8 @@ class SendStatusService : Service() { } private fun cancelSendingIntent(statusId: Int): PendingIntent { + // TODO: Revisit suppressing this when this file is moved + @SuppressLint("IntentDetector") val intent = Intent(this, SendStatusService::class.java) intent.putExtra(KEY_CANCEL, statusId) return PendingIntent.getService( @@ -428,6 +431,8 @@ class SendStatusService : Service() { context: Context, statusToSend: StatusToSend, ): Intent { + // TODO: Revisit suppressing this when this file is moved + @SuppressLint("IntentDetector") val intent = Intent(context, SendStatusService::class.java) intent.putExtra(KEY_STATUS, statusToSend) diff --git a/app/src/main/java/app/pachli/util/ShareShortcutHelper.kt b/app/src/main/java/app/pachli/util/ShareShortcutHelper.kt index 403d991ca..91ec2e0b0 100644 --- a/app/src/main/java/app/pachli/util/ShareShortcutHelper.kt +++ b/app/src/main/java/app/pachli/util/ShareShortcutHelper.kt @@ -25,9 +25,9 @@ import androidx.core.app.Person import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat -import app.pachli.MainActivity import app.pachli.R import app.pachli.core.database.model.AccountEntity +import app.pachli.core.navigation.MainActivityIntent import com.bumptech.glide.Glide import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers @@ -67,7 +67,7 @@ fun updateShortcut(context: Context, account: AccountEntity) { .build() // This intent will be sent when the user clicks on one of the launcher shortcuts. Intent from share sheet will be different - val intent = Intent(context, MainActivity::class.java).apply { + val intent = MainActivityIntent(context).apply { action = Intent.ACTION_SEND type = "text/plain" putExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID, account.id.toString()) diff --git a/app/src/test/java/app/pachli/MainActivityTest.kt b/app/src/test/java/app/pachli/MainActivityTest.kt index feb3a9d55..4b1564091 100644 --- a/app/src/test/java/app/pachli/MainActivityTest.kt +++ b/app/src/test/java/app/pachli/MainActivityTest.kt @@ -33,6 +33,7 @@ import app.pachli.core.accounts.AccountManager import app.pachli.core.database.model.AccountEntity import app.pachli.core.database.model.TabKind import app.pachli.core.database.model.defaultTabs +import app.pachli.core.navigation.AccountListActivityIntent import app.pachli.core.network.model.Account import app.pachli.core.network.model.Notification import app.pachli.core.network.model.TimelineAccount @@ -176,7 +177,7 @@ class MainActivityTest { nextActivity.component, ) assertEquals( - AccountListActivity.Type.FOLLOW_REQUESTS, + AccountListActivityIntent.Kind.FOLLOW_REQUESTS, nextActivity.getSerializableExtra("type"), ) } diff --git a/app/src/test/java/app/pachli/components/compose/ComposeActivityTest.kt b/app/src/test/java/app/pachli/components/compose/ComposeActivityTest.kt index a227f9f0b..6763d3bbc 100644 --- a/app/src/test/java/app/pachli/components/compose/ComposeActivityTest.kt +++ b/app/src/test/java/app/pachli/components/compose/ComposeActivityTest.kt @@ -24,6 +24,8 @@ import app.pachli.PachliApplication import app.pachli.R import app.pachli.components.instanceinfo.InstanceInfoRepository import app.pachli.core.accounts.AccountManager +import app.pachli.core.navigation.ComposeActivityIntent +import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions import app.pachli.core.network.model.Account import app.pachli.core.network.model.InstanceConfiguration import app.pachli.core.network.model.InstanceV1 @@ -138,7 +140,7 @@ class ComposeActivityTest { @Test fun whenModifiedInitialState_andCloseButtonPressed_notFinish() { - rule.launch(intent(ComposeActivity.ComposeOptions(modifiedInitialState = true))) + rule.launch(intent(ComposeOptions(modifiedInitialState = true))) rule.getScenario().onActivity { clickUp(it) assertFalse(it.isFinishing) @@ -167,7 +169,7 @@ class ComposeActivityTest { @Test fun whenModifiedInitialState_andBackButtonPressed_notFinish() { - rule.launch(intent(ComposeActivity.ComposeOptions(modifiedInitialState = true))) + rule.launch(intent(ComposeOptions(modifiedInitialState = true))) rule.getScenario().onActivity { clickBack(it) assertFalse(it.isFinishing) @@ -512,7 +514,7 @@ class ComposeActivityTest { @Test fun languageGivenInComposeOptionsIsRespected() { - rule.launch(intent(ComposeActivity.ComposeOptions(language = "no"))) + rule.launch(intent(ComposeOptions(language = "no"))) rule.getScenario().onActivity { assertEquals("no", it.selectedLanguage) } @@ -522,7 +524,7 @@ class ComposeActivityTest { fun modernLanguageCodeIsUsed() { // https://github.com/tuskyapp/Tusky/issues/2903 // "ji" was deprecated in favor of "yi" - rule.launch(intent(ComposeActivity.ComposeOptions(language = "ji"))) + rule.launch(intent(ComposeOptions(language = "ji"))) rule.getScenario().onActivity { assertEquals("yi", it.selectedLanguage) } @@ -530,14 +532,14 @@ class ComposeActivityTest { @Test fun unknownLanguageGivenInComposeOptionsIsRespected() { - rule.launch(intent(ComposeActivity.ComposeOptions(language = "zzz"))) + rule.launch(intent(ComposeOptions(language = "zzz"))) rule.getScenario().onActivity { assertEquals("zzz", it.selectedLanguage) } } /** Returns an intent to launch [ComposeActivity] with the given options */ - private fun intent(composeOptions: ComposeActivity.ComposeOptions) = ComposeActivity.startIntent( + private fun intent(composeOptions: ComposeOptions) = ComposeActivityIntent( ApplicationProvider.getApplicationContext(), composeOptions, ) diff --git a/build.gradle b/build.gradle index 67b38bf3f..05d6769d1 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ plugins { alias(libs.plugins.ktlint) apply false alias(libs.plugins.aboutlibraries) apply false alias(libs.plugins.hilt) apply false + alias(libs.plugins.quadrant) apply false } allprojects { diff --git a/checks/src/main/java/app/pachli/lint/checks/IntentDetector.kt b/checks/src/main/java/app/pachli/lint/checks/IntentDetector.kt new file mode 100644 index 000000000..a712b4938 --- /dev/null +++ b/checks/src/main/java/app/pachli/lint/checks/IntentDetector.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2023 Pachli Association + * + * This file is a part of Pachli. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Pachli; if not, + * see . + */ + +package app.pachli.lint.checks + +import com.android.SdkConstants.CLASS_INTENT +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.getQualifiedName +import org.jetbrains.uast.util.isConstructorCall + +class IntentDetector : Detector(), Detector.UastScanner { + override fun getApplicableUastTypes() = listOf(UCallExpression::class.java) + + override fun createUastHandler(context: JavaContext) = object : UElementHandler() { + override fun visitCallExpression(node: UCallExpression) { + // Ignore anything that is not constructing an Intent + if (!node.isConstructorCall()) return + val classRef = node.classReference ?: return + val className = classRef.getQualifiedName() + if (className != CLASS_INTENT) return + + // Ignore calls that don't have 2 or 4 parameters + val constructor = node.resolve() ?: return + val parameters = constructor.parameterList.parameters + if (parameters.size != 2 && parameters.size != 4) return + + // Ignore calls where the last parameter is not a class literal + val lastParam = parameters.last() + if (lastParam.type.canonicalText != "java.lang.Class") return + + context.report( + issue = ISSUE, + scope = node, + location = context.getCallLocation(node, true, true), + message = "Use functions from `core.navigation`", + ) + } + } + + companion object { + val ISSUE = Issue.create( + id = "IntentDetector", + briefDescription = "Don't use `Intent(...)`, use functions from core.navigation", + explanation = """ + Creating an `Intent` with a class from another module can create unnecessary or circular + dependencies. Use the `...Intent` classes in `core.navigation` to create an intent for + the appropriate `Activity`. + """.trimIndent(), + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + IntentDetector::class.java, + Scope.JAVA_FILE_SCOPE, + ), + ) + } +} diff --git a/checks/src/main/java/app/pachli/lint/checks/LintRegistry.kt b/checks/src/main/java/app/pachli/lint/checks/LintRegistry.kt index c0cfde5fc..23f343381 100644 --- a/checks/src/main/java/app/pachli/lint/checks/LintRegistry.kt +++ b/checks/src/main/java/app/pachli/lint/checks/LintRegistry.kt @@ -7,7 +7,10 @@ import com.android.tools.lint.detector.api.Issue @Suppress("UnstableApiUsage") class LintRegistry : IssueRegistry() { override val issues: List - get() = listOf(AndroidxToolbarDetector.ISSUE) + get() = listOf( + AndroidxToolbarDetector.ISSUE, + IntentDetector.ISSUE, + ) override val api: Int get() = CURRENT_API diff --git a/checks/src/test/java/app/pachli/lint/checks/IntentDetectorTest.kt b/checks/src/test/java/app/pachli/lint/checks/IntentDetectorTest.kt new file mode 100644 index 000000000..6d73e9007 --- /dev/null +++ b/checks/src/test/java/app/pachli/lint/checks/IntentDetectorTest.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2023 Pachli Association + * + * This file is a part of Pachli. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Pachli; if not, + * see . + */ + +package app.pachli.lint.checks + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +class IntentDetectorTest : LintDetectorTest() { + override fun getDetector(): Detector = IntentDetector() + + override fun getIssues(): List = listOf(IntentDetector.ISSUE) + + fun `test Intent component constructor emits warning`() { + lint().files( + Context, + Intent, + kotlin( + """ + package test.pkg + + import android.content.Context + import android.content.Intent + + fun makeIntent(context: Context) = Intent(context, String::class.java) + """, + + ).indented(), + ).allowMissingSdk().run().expect( + """src/test/pkg/test.kt:6: Warning: Use functions from core.navigation [IntentDetector] +fun makeIntent(context: Context) = Intent(context, String::class.java) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +0 errors, 1 warnings""", + ) + } + + fun `test Intent action and data component constructor emits warning`() { + lint().files( + Context, + Intent, + kotlin( + """ + package test.pkg + + import android.content.Context + import android.content.Intent + + fun makeIntent(context: Context) = Intent( + "someAction", + Uri.parse("https://example.com"), + context, + String::class.java, + ) + """, + ).indented(), + ).allowMissingSdk().run().expect( + """src/test/pkg/test.kt:6: Warning: Use functions from core.navigation [IntentDetector] +fun makeIntent(context: Context) = Intent( + ^ +0 errors, 1 warnings""", + ) + } + + fun `test empty constructor does not warn`() { + lint().files( + Context, + Intent, + kotlin( + """ + package test.pkg + + import android.content.Intent + + fun makeIntent() = Intent() + """, + ).indented(), + ).allowMissingSdk().run().expectClean() + } + + fun `test copy constructor does not warn`() { + lint().files( + Context, + Intent, + kotlin( + """ + package test.pkg + + import android.content.Intent + + fun makeIntent(intent: Intent) = Intent(intent, 0) + """, + ).indented(), + ).allowMissingSdk().run().expectClean() + } + + fun `test action constructor does not warn`() { + lint().files( + Context, + Intent, + kotlin( + """ + package test.pkg + + import android.content.Intent + + fun makeIntent() = Intent("some action") + """, + ).indented(), + ).allowMissingSdk().run().expectClean() + } + + fun `test action and uri constructor does not warn`() { + lint().files( + Context, + Intent, + kotlin( + """ + package test.pkg + + import android.content.Intent + + fun makeIntent() = Intent("some action", Uri.parse("http://example.com")) + """, + ).indented(), + ).allowMissingSdk().run().expectClean() + } + + companion object Stubs { + /** Stub for `android.content.Context` */ + private val Context = java( + """ + package android.content; + + public class Context {} + """, + ).indented() + + /** Stub for `android.content.Intent` */ + private val Intent = java( + """ + package android.content; + + import android.content.Context; + + public class Intent { + public Intent() { return null; } + public Intent(Intent o, int copyMode) { return null; } + public Intent(String action) { return null; } + public Intent(String action, Uri uri) { return null; } + public Intent(Context packageContext, Class cls) { return null; } + public Intent(String action, Uri uri, Context packageContext, Class cls) { return null; } + } + """, + ).indented() + } +} diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 07e5c982b..d0c33575c 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -36,5 +36,5 @@ dependencies { implementation(projects.core.preferences) // Because of the use of @SerializedName in DraftEntity - compileOnly(libs.gson) + implementation(libs.gson) } diff --git a/core/navigation/README.md b/core/navigation/README.md new file mode 100644 index 000000000..4e0cc3843 --- /dev/null +++ b/core/navigation/README.md @@ -0,0 +1,23 @@ +# :core:navigation + +## package app.pachli.core.navigation + +Intents for starting activities to break circular dependencies. + +A common approach for surfacing type-safe (ish) intents to start activities is for the activity-to-be-launched to provide a method in a companion object that returns the relevant intent, possibly taking additional parameters that will be included in the intent as extras. + +E.g., if A wants to start B, B provides the method that returns the intent. + +This introduces a dependency between A and B. + +This is worse if B also wants to start A. + +For example, if A is `StatusListActivity` and B is`ViewThreadActivity`. The user might click a status in `StatusListActivity` to view the thread, starting `ViewThreadActivity`. But from the thread they might click a hashtag to view the list of statuses with that hashtag. Now `StatusListActivity` and `ViewThreadActivity` have a circular dependency. + +Even if that doesn't happen the dependency means that any changes to B will trigger a rebuild of A, even if the changes to B are not relevant. + +This package contains `Intent` subclasses that should be used instead. The `quadrant` plugin is used to generate constants that can be used to launch activities by name instead of by class, breaking the dependency chain. + +If the activity's intent requires specific extras those are passed via the constructor, with companion object methods to extract them from the intent. + +Using the intent classes from this package is enforced by a lint `IntentDetector` which will warn if any intents are created using a class literal. diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts new file mode 100644 index 000000000..a7e28fd7a --- /dev/null +++ b/core/navigation/build.gradle.kts @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Pachli Association + * + * This file is a part of Pachli. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Pachli; if not, + * see . + */ + +plugins { + alias(libs.plugins.pachli.android.library) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.quadrant) +} + +android { + namespace = "app.pachli.core.navigation" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + implementation(projects.core.database) // For DraftAttachment, used in ComposeOptions + implementation(projects.core.network) // For Attachment, used in AttachmentViewData + + implementation(libs.androidx.core.ktx) // IntentCompat +} diff --git a/core/navigation/lint-baseline.xml b/core/navigation/lint-baseline.xml new file mode 100644 index 000000000..ddc2d3f41 --- /dev/null +++ b/core/navigation/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/navigation/src/main/AndroidManifest.xml b/core/navigation/src/main/AndroidManifest.xml new file mode 100644 index 000000000..76e9ba6e7 --- /dev/null +++ b/core/navigation/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/main/java/app/pachli/viewdata/AttachmentViewData.kt b/core/navigation/src/main/kotlin/app/pachli/core/navigation/AttachmentViewData.kt similarity index 95% rename from app/src/main/java/app/pachli/viewdata/AttachmentViewData.kt rename to core/navigation/src/main/kotlin/app/pachli/core/navigation/AttachmentViewData.kt index 504c074fd..daa947027 100644 --- a/app/src/main/java/app/pachli/viewdata/AttachmentViewData.kt +++ b/core/navigation/src/main/kotlin/app/pachli/core/navigation/AttachmentViewData.kt @@ -1,4 +1,5 @@ -/* Copyright 2022 Tusky Contributors +/* + * Copyright 2022 Tusky Contributors * * This file is a part of Pachli. * @@ -14,7 +15,7 @@ * see . */ -package app.pachli.viewdata +package app.pachli.core.navigation import android.os.Parcelable import app.pachli.core.network.model.Attachment diff --git a/core/navigation/src/main/kotlin/app/pachli/core/navigation/Navigation.kt b/core/navigation/src/main/kotlin/app/pachli/core/navigation/Navigation.kt new file mode 100644 index 000000000..48bfd3b32 --- /dev/null +++ b/core/navigation/src/main/kotlin/app/pachli/core/navigation/Navigation.kt @@ -0,0 +1,482 @@ +/* + * Copyright 2023 Pachli Association + * + * This file is a part of Pachli. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Pachli; if not, + * see . + */ + +package app.pachli.core.navigation + +import android.content.Context +import android.content.Intent +import android.os.Parcelable +import androidx.core.content.IntentCompat +import app.pachli.core.database.model.DraftAttachment +import app.pachli.core.navigation.LoginActivityIntent.LoginMode +import app.pachli.core.navigation.StatusListActivityIntent.Companion.bookmarks +import app.pachli.core.navigation.StatusListActivityIntent.Companion.favourites +import app.pachli.core.navigation.StatusListActivityIntent.Companion.hashtag +import app.pachli.core.navigation.StatusListActivityIntent.Companion.list +import app.pachli.core.network.model.Attachment +import app.pachli.core.network.model.Filter +import app.pachli.core.network.model.NewPoll +import app.pachli.core.network.model.Status +import app.pachli.core.network.model.TimelineKind +import com.gaelmarhic.quadrant.QuadrantConstants +import kotlinx.parcelize.Parcelize + +/** + * @param context + * @param accountId Server ID of the account to view + * @see [app.pachli.components.account.AccountActivity] + */ +class AccountActivityIntent(context: Context, accountId: String) : Intent() { + init { + setClassName(context, "app.pachli${QuadrantConstants.ACCOUNT_ACTIVITY}") + putExtra(EXTRA_KEY_ACCOUNT_ID, accountId) + } + + companion object { + private const val EXTRA_KEY_ACCOUNT_ID = "id" + + /** @return the account ID passed in this intent */ + fun getAccountId(intent: Intent) = intent.getStringExtra(EXTRA_KEY_ACCOUNT_ID)!! + } +} + +/** + * @param context + * @param kind The kind of accounts to show + * @param id Optional ID. Sometimes an account ID, sometimes a status ID, and + * sometimes ignored. See [Kind] for details of how `id` is interpreted. + * @see [app.pachli.components.accountlist.AccountListActivity] + */ +class AccountListActivityIntent(context: Context, kind: Kind, id: String? = null) : Intent() { + enum class Kind { + /** Show the accounts the account with `id` is following */ + FOLLOWS, + + /** Show the accounts following the account with `id` */ + FOLLOWERS, + + /** Show the accounts the account with `id` is blocking */ + BLOCKS, + + /** Show the accounts the account with `id` is muting */ + MUTES, + + /** Show the logged in account's follow requests (`id` is ignored) */ + FOLLOW_REQUESTS, + + /** Show the accounts that reblogged the status with `id` */ + REBLOGGED, + + /** Show the accounts that favourited the status with `id` */ + FAVOURITED, + } + + init { + setClassName(context, "app.pachli${QuadrantConstants.ACCOUNT_LIST_ACTIVITY}") + putExtra(EXTRA_KIND, kind) + putExtra(EXTRA_ID, id) + } + + companion object { + private const val EXTRA_KIND = "kind" + private const val EXTRA_ID = "id" + + /** @return The [Kind] passed in this intent */ + fun getKind(intent: Intent) = intent.getSerializableExtra(EXTRA_KIND) as Kind + + /** @return The ID passed in this intent, or null */ + fun getId(intent: Intent) = intent.getStringExtra(EXTRA_ID) + } +} + +/** + * @param context + * @see [app.pachli.components.compose.ComposeActivity] + */ +class ComposeActivityIntent(context: Context) : Intent() { + @Parcelize + data class ComposeOptions( + val scheduledTootId: String? = null, + val draftId: Int? = null, + val content: String? = null, + val mediaUrls: List? = null, + val mediaDescriptions: List? = null, + val mentionedUsernames: Set? = null, + val inReplyToId: String? = null, + val replyVisibility: Status.Visibility? = null, + val visibility: Status.Visibility? = null, + val contentWarning: String? = null, + val replyingStatusAuthor: String? = null, + val replyingStatusContent: String? = null, + val mediaAttachments: List? = null, + val draftAttachments: List? = null, + val scheduledAt: String? = null, + val sensitive: Boolean? = null, + val poll: NewPoll? = null, + val modifiedInitialState: Boolean? = null, + val language: String? = null, + val statusId: String? = null, + val kind: ComposeKind? = null, + val initialCursorPosition: InitialCursorPosition = InitialCursorPosition.END, + ) : Parcelable { + /** + * Status' kind. This particularly affects how the status is handled if the user + * backs out of the edit. + */ + enum class ComposeKind { + /** Status is new */ + NEW, + + /** Editing a posted status */ + EDIT_POSTED, + + /** Editing a status started as an existing draft */ + EDIT_DRAFT, + + /** Editing an an existing scheduled status */ + EDIT_SCHEDULED, + } + + /** + * Initial position of the cursor in EditText when the compose button is clicked + * in a hashtag timeline + */ + enum class InitialCursorPosition { + /** Position the cursor at the start of the line */ + START, + + /** Position the cursor at the end of the line */ + END, + } + } + + init { + setClassName(context, "app.pachli${QuadrantConstants.COMPOSE_ACTIVITY}") + } + + /** + * @param context + * @param options Configure the initial state of the activity + * @see [app.pachli.components.compose.ComposeActivity] + */ + constructor(context: Context, options: ComposeOptions) : this(context) { + putExtra(EXTRA_COMPOSE_OPTIONS, options) + } + + companion object { + private const val EXTRA_COMPOSE_OPTIONS = "composeOptions" + + /** @return the [ComposeOptions] passed in this intent, or null */ + fun getOptions(intent: Intent) = IntentCompat.getParcelableExtra(intent, EXTRA_COMPOSE_OPTIONS, ComposeOptions::class.java) + } +} + +/** + * @param context + * @param filter Optional filter to edit. If null an empty filter is created. + * @see [app.pachli.components.filters.EditFilterActivity] + */ +class EditFilterActivityIntent(context: Context, filter: Filter? = null) : Intent() { + init { + setClassName(context, "app.pachli${QuadrantConstants.EDIT_FILTER_ACTIVITY}") + filter?.let { + putExtra(EXTRA_FILTER_TO_EDIT, it) + } + } + + companion object { + const val EXTRA_FILTER_TO_EDIT = "filterToEdit" + + /** @return the [Filter] passed in this intent, or null */ + fun getFilter(intent: Intent) = IntentCompat.getParcelableExtra(intent, EXTRA_FILTER_TO_EDIT, Filter::class.java) + } +} + +/** + * @param context + * @param loginMode See [LoginMode] + * @see [app.pachli.components.login.LoginActivity] + */ +class LoginActivityIntent(context: Context, loginMode: LoginMode = LoginMode.DEFAULT) : Intent() { + /** How to log in */ + enum class LoginMode { + DEFAULT, + + /** Already logged in, log in with an additional account */ + ADDITIONAL_LOGIN, + + /** Update the OAuth scope granted to the client */ + MIGRATION, + } + + init { + setClassName(context, "app.pachli${QuadrantConstants.LOGIN_ACTIVITY}") + putExtra(EXTRA_LOGIN_MODE, loginMode) + } + + companion object { + private const val EXTRA_LOGIN_MODE = "loginMode" + + /** @return the `loginMode` passed to this intent */ + fun getLoginMode(intent: Intent) = intent.getSerializableExtra(EXTRA_LOGIN_MODE)!! as LoginMode + } +} + +/** + * @param context + * @param screen The preference screen to show + * @see [app.pachli.components.preference.PreferencesActivity] + */ +class PreferencesActivityIntent(context: Context, screen: PreferenceScreen) : Intent() { + /** A specific preference screen */ + enum class PreferenceScreen { + /** General preferences */ + GENERAL, + + /** Account-specific preferences */ + ACCOUNT, + + /** Notification preferences */ + NOTIFICATION, + } + init { + setClassName(context, "app.pachli${QuadrantConstants.PREFERENCES_ACTIVITY}") + putExtra(EXTRA_PREFERENCE_SCREEN, screen) + } + + companion object { + private const val EXTRA_PREFERENCE_SCREEN = "preferenceScreen" + + /** @return the `screen` passed to this intent */ + fun getPreferenceType(intent: Intent) = intent.getSerializableExtra(EXTRA_PREFERENCE_SCREEN)!! as PreferenceScreen + } +} + +/** + * @param context + * @param accountId The ID of the account to report + * @param userName The username of the account to report + * @param statusId Optional ID of a status to include in the report + * @see [app.pachli.components.report.ReportActivity] + */ +class ReportActivityIntent(context: Context, accountId: String, userName: String, statusId: String? = null) : Intent() { + init { + setClassName(context, "app.pachli${QuadrantConstants.REPORT_ACTIVITY}") + putExtra(EXTRA_ACCOUNT_ID, accountId) + putExtra(EXTRA_ACCOUNT_USERNAME, userName) + putExtra(EXTRA_STATUS_ID, statusId) + } + + companion object { + private const val EXTRA_ACCOUNT_ID = "accountId" + private const val EXTRA_ACCOUNT_USERNAME = "accountUsername" + private const val EXTRA_STATUS_ID = "statusId" + + /** @return the `accountId` passed to this intent */ + fun getAccountId(intent: Intent) = intent.getStringExtra(EXTRA_ACCOUNT_ID)!! + /** @return the `userName` passed to this intent */ + fun getAccountUserName(intent: Intent) = intent.getStringExtra(EXTRA_ACCOUNT_USERNAME)!! + /** @return the `statusId` passed to this intent, or null */ + fun getStatusId(intent: Intent) = intent.getStringExtra(EXTRA_STATUS_ID) + } +} + +/** + * Use one of [bookmarks], [favourites], [hashtag], or [list] to construct. + */ +class StatusListActivityIntent private constructor (context: Context) : Intent() { + init { + setClassName(context, "app.pachli${QuadrantConstants.STATUS_LIST_ACTIVITY}") + } + + companion object { + private const val EXTRA_KIND = "kind" + + /** + * Show the user's bookmarks. + * + * @param context + */ + fun bookmarks(context: Context) = StatusListActivityIntent(context).apply { + putExtra(EXTRA_KIND, TimelineKind.Bookmarks) + } + + /** + * Show the user's favourites. + * + * @param context + */ + fun favourites(context: Context) = StatusListActivityIntent(context).apply { + putExtra(EXTRA_KIND, TimelineKind.Favourites) + } + + /** + * Show statuses containing [hashtag]. + * + * @param context + * @param hashtag The hashtag to show, without the leading "`#`" + */ + fun hashtag(context: Context, hashtag: String) = StatusListActivityIntent(context).apply { + putExtra(EXTRA_KIND, TimelineKind.Tag(listOf(hashtag))) + } + + /** + * Show statuses from a list. + * + * @param context + * @param listId ID of the list to show + * @param title The title to display + */ + fun list(context: Context, listId: String, title: String) = StatusListActivityIntent(context).apply { + putExtra(EXTRA_KIND, TimelineKind.UserList(listId, title)) + } + + /** @return The [TimelineKind] to show */ + fun getKind(intent: Intent) = IntentCompat.getParcelableExtra(intent, EXTRA_KIND, TimelineKind::class.java)!! + } +} + +class ViewMediaActivityIntent private constructor(context: Context) : Intent() { + init { + setClassName(context, "app.pachli${QuadrantConstants.VIEW_MEDIA_ACTIVITY}") + } + + /** + * Show a collection of media attachments. + * + * @param context + * @param attachments The attachments to show + * @param index The index of the attachment in [attachments] to focus on + */ + constructor(context: Context, attachments: List, index: Int) : this(context) { + putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments)) + putExtra(EXTRA_ATTACHMENT_INDEX, index) + } + + /** + * Show a single image identified by a URL + * + * @param context + * @param url The URL of the image + */ + constructor(context: Context, url: String) : this(context) { + putExtra(EXTRA_SINGLE_IMAGE_URL, url) + } + + companion object { + private const val EXTRA_ATTACHMENTS = "attachments" + private const val EXTRA_ATTACHMENT_INDEX = "index" + private const val EXTRA_SINGLE_IMAGE_URL = "singleImage" + + /** @return the list of [AttachmentViewData] passed in this intent, or null */ + fun getAttachments(intent: Intent): ArrayList? = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_ATTACHMENTS, AttachmentViewData::class.java) + + /** @return the index of the attachment to show, or 0 */ + fun getAttachmentIndex(intent: Intent) = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0) + + /** @return the URL of the single image to show, null if no URL was included */ + fun getImageUrl(intent: Intent) = intent.getStringExtra(EXTRA_SINGLE_IMAGE_URL) + } +} + +/** + * @param context + * @param statusId ID of the status to start from (may be in the middle of the thread) + * @param statusUrl Optional URL of the status in `statusId` + * @see [app.pachli.components.viewthread.ViewThreadActivity] + */ +class ViewThreadActivityIntent(context: Context, statusId: String, statusUrl: String? = null) : Intent() { + init { + setClassName(context, "app.pachli${QuadrantConstants.VIEW_THREAD_ACTIVITY}") + putExtra(EXTRA_STATUS_ID, statusId) + putExtra(EXTRA_STATUS_URL, statusUrl) + } + + companion object { + private const val EXTRA_STATUS_ID = "id" + private const val EXTRA_STATUS_URL = "url" + + /** @return the `statusId` passed to this intent */ + fun getStatusId(intent: Intent) = intent.getStringExtra(EXTRA_STATUS_ID)!! + /** @return the `statusUrl` passed to this intent, or null */ + fun getUrl(intent: Intent) = intent.getStringExtra(EXTRA_STATUS_URL) + } +} + +class AboutActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.ABOUT_ACTIVITY}") } +} + +class AnnouncementsActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.ANNOUNCEMENTS_ACTIVITY}") } +} + +class DraftsActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.DRAFTS_ACTIVITY}") } +} + +class EditProfileActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.EDIT_PROFILE_ACTIVITY}") } +} + +class FiltersActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.FILTERS_ACTIVITY}") } +} + +class FollowedTagsActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.FOLLOWED_TAGS_ACTIVITY}") } +} + +class InstanceListActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.INSTANCE_LIST_ACTIVITY}") } +} + +class LicenseActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.LICENSE_ACTIVITY}") } +} + +class ListActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.LISTS_ACTIVITY}") } +} + +class LoginWebViewActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.LOGIN_WEB_VIEW_ACTIVITY}") } +} + +class MainActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.MAIN_ACTIVITY}") } +} + +class PrivacyPolicyActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.PRIVACY_POLICY_ACTIVITY}") } +} + +class ScheduledStatusActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.SCHEDULED_STATUS_ACTIVITY}") } +} + +class SearchActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.SEARCH_ACTIVITY}") } +} + +class TabPreferenceActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.TAB_PREFERENCE_ACTIVITY}") } +} + +class TrendingActivityIntent(context: Context) : Intent() { + init { setClassName(context, "app.pachli${QuadrantConstants.TRENDING_ACTIVITY}") } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 67d7e53d9..d97456ffe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,6 +50,7 @@ mockito-inline = "5.2.0" mockito-kotlin = "5.2.1" networkresult-calladapter = "1.0.0" okhttp = "4.12.0" +quadrant = "1.7" retrofit = "2.9.0" robolectric = "4.11.1" rxandroid3 = "3.0.2" @@ -76,6 +77,7 @@ kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } ktlint = "org.jlleitschuh.gradle.ktlint:11.6.1" room = { id = "androidx.room", version.ref = "androidx-room" } +quadrant = { id = "com.gaelmarhic.quadrant", version.ref = "quadrant" } # Plugins defined by this project pachli-android-application = { id = "pachli.android.application", version = "unspecified" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 3c8e22ee0..76ae589ce 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,6 +27,7 @@ include(":core:accounts") include(":core:common") include(":core:database") include(":core:preferences") +include(":core:navigation") include(":core:network") include(":core:testing") include(":tools:mklanguages")