From 1214cf7c8afaae22ce22689bd5f677ae51ccf82f Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Thu, 7 Dec 2023 18:36:00 +0100 Subject: [PATCH] refactor: Break navigation dependency cycles with :core:navigation (#305) The previous code generally started an activity by having the activity 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 that starts B. 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. Break this dependency by adding a `:core:navigation` module with an `app.pachli.core.navigation` package that 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. The plugin uses the `Activity` names from the manifest, so when an activity is moved in the future the constant will automatically update to reflect the new package name. 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. See #291 --- app/build.gradle.kts | 1 + app/lint-baseline.xml | 194 +++---- app/src/main/java/app/pachli/AboutActivity.kt | 8 +- app/src/main/java/app/pachli/BaseActivity.kt | 4 +- .../java/app/pachli/BottomSheetActivity.kt | 11 +- app/src/main/java/app/pachli/ListsActivity.kt | 9 +- app/src/main/java/app/pachli/MainActivity.kt | 89 ++-- .../main/java/app/pachli/SplashActivity.kt | 9 +- .../java/app/pachli/StatusListActivity.kt | 44 +- .../java/app/pachli/TabPreferenceActivity.kt | 4 +- .../main/java/app/pachli/ViewMediaActivity.kt | 39 +- .../pachli/adapter/StatusBaseViewHolder.kt | 4 +- .../components/account/AccountActivity.kt | 54 +- .../account/media/AccountMediaFragment.kt | 6 +- .../account/media/AccountMediaGridAdapter.kt | 2 +- .../account/media/AccountMediaPagingSource.kt | 2 +- .../media/AccountMediaRemoteMediator.kt | 2 +- .../account/media/AccountMediaViewModel.kt | 2 +- .../accountlist/AccountListActivity.kt | 57 +-- .../accountlist/AccountListFragment.kt | 63 ++- .../announcements/AnnouncementsActivity.kt | 10 +- .../components/compose/ComposeActivity.kt | 82 +-- .../components/compose/ComposeViewModel.kt | 5 +- .../conversation/ConversationsFragment.kt | 10 +- .../components/drafts/DraftsActivity.kt | 20 +- .../components/filters/EditFilterActivity.kt | 9 +- .../components/filters/FiltersActivity.kt | 8 +- .../pachli/components/login/LoginActivity.kt | 29 +- .../components/login/LoginWebViewActivity.kt | 3 +- .../notifications/NotificationHelper.kt | 27 +- .../notifications/NotificationsFragment.kt | 2 +- .../notifications/PushNotificationHelper.kt | 10 +- .../preference/AccountPreferencesFragment.kt | 33 +- .../preference/PreferencesActivity.kt | 29 +- .../components/report/ReportActivity.kt | 31 +- .../fragments/ReportStatusesFragment.kt | 14 +- .../scheduled/ScheduledStatusActivity.kt | 15 +- .../components/search/SearchActivity.kt | 4 - .../search/fragments/SearchFragment.kt | 8 +- .../fragments/SearchStatusesFragment.kt | 28 +- .../components/timeline/TimelineFragment.kt | 9 +- .../components/trending/TrendingActivity.kt | 6 - .../trending/TrendingTagsFragment.kt | 4 +- .../viewthread/ViewThreadActivity.kt | 19 +- .../viewthread/ViewThreadFragment.kt | 11 +- .../viewthread/edits/ViewEditsFragment.kt | 8 +- .../main/java/app/pachli/db/DraftsAlert.kt | 4 +- .../java/app/pachli/fragment/SFragment.kt | 31 +- .../app/pachli/service/PachliTileService.kt | 4 +- .../app/pachli/service/SendStatusService.kt | 5 + .../app/pachli/util/ShareShortcutHelper.kt | 4 +- .../test/java/app/pachli/MainActivityTest.kt | 3 +- .../components/compose/ComposeActivityTest.kt | 14 +- build.gradle | 1 + .../app/pachli/lint/checks/IntentDetector.kt | 80 +++ .../app/pachli/lint/checks/LintRegistry.kt | 5 +- .../pachli/lint/checks/IntentDetectorTest.kt | 171 +++++++ core/database/build.gradle.kts | 2 +- core/navigation/README.md | 23 + core/navigation/build.gradle.kts | 37 ++ core/navigation/lint-baseline.xml | 4 + core/navigation/src/main/AndroidManifest.xml | 21 + .../core/navigation}/AttachmentViewData.kt | 5 +- .../app/pachli/core/navigation/Navigation.kt | 482 ++++++++++++++++++ gradle/libs.versions.toml | 2 + settings.gradle.kts | 1 + 66 files changed, 1299 insertions(+), 638 deletions(-) create mode 100644 checks/src/main/java/app/pachli/lint/checks/IntentDetector.kt create mode 100644 checks/src/test/java/app/pachli/lint/checks/IntentDetectorTest.kt create mode 100644 core/navigation/README.md create mode 100644 core/navigation/build.gradle.kts create mode 100644 core/navigation/lint-baseline.xml create mode 100644 core/navigation/src/main/AndroidManifest.xml rename {app/src/main/java/app/pachli/viewdata => core/navigation/src/main/kotlin/app/pachli/core/navigation}/AttachmentViewData.kt (95%) create mode 100644 core/navigation/src/main/kotlin/app/pachli/core/navigation/Navigation.kt 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")