diff --git a/changelog.d/6548.feature b/changelog.d/6548.feature new file mode 100644 index 0000000000..8c40a37063 --- /dev/null +++ b/changelog.d/6548.feature @@ -0,0 +1 @@ +Move initialization of the Session to a background thread. MainActivity is restoring the session now, instead of VectorApplication. Useful when for instance a long migration of a database is required. diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 1c104f3bbf..b7bdac6879 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -380,6 +380,11 @@ android:exported="false" android:foregroundServiceType="location" /> + + + @Binds + @IntoMap + @MavericksViewModelKey(StartAppViewModel::class) + fun startAppViewModelFactory(factory: StartAppViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds @IntoMap @MavericksViewModelKey(HomeServerCapabilitiesViewModel::class) diff --git a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt index 53a5470ff7..be84dfeaba 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/VectorMessagingReceiver.kt @@ -27,6 +27,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import dagger.hilt.android.AndroidEntryPoint import im.vector.app.BuildConfig import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.ActiveSessionSetter import im.vector.app.core.network.WifiDetector import im.vector.app.core.pushers.model.PushData import im.vector.app.core.services.GuardServiceStarter @@ -59,6 +60,7 @@ class VectorMessagingReceiver : MessagingReceiver() { @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notifiableEventResolver: NotifiableEventResolver @Inject lateinit var pushersManager: PushersManager + @Inject lateinit var activeSessionSetter: ActiveSessionSetter @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorDataStore: VectorDataStore @@ -177,6 +179,11 @@ class VectorMessagingReceiver : MessagingReceiver() { } val session = activeSessionHolder.getSafeActiveSession() + ?: run { + // Active session may not exist yet, if MainActivity has not been launched + activeSessionSetter.tryToSetActiveSession(startSync = false) + activeSessionHolder.getSafeActiveSession() + } if (session == null) { Timber.tag(loggerTag.value).w("## Can't sync from push, no current session") diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index c2f6f2d778..61127e2c82 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -17,11 +17,15 @@ package im.vector.app.features import android.app.Activity +import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Parcelable +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import com.airbnb.mvrx.viewModel import com.bumptech.glide.Glide import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint @@ -44,9 +48,16 @@ import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.session.VectorSessionStore import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.signout.hard.SignedOutActivity +import im.vector.app.features.start.StartAppAction +import im.vector.app.features.start.StartAppAndroidService +import im.vector.app.features.start.StartAppViewEvent +import im.vector.app.features.start.StartAppViewModel +import im.vector.app.features.start.StartAppViewState import im.vector.app.features.themes.ActivityOtherThemes import im.vector.app.features.ui.UiStateRepository import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @@ -73,6 +84,8 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity companion object { private const val EXTRA_ARGS = "EXTRA_ARGS" + private const val EXTRA_NEXT_INTENT = "EXTRA_NEXT_INTENT" + private const val EXTRA_INIT_SESSION = "EXTRA_INIT_SESSION" // Special action to clear cache and/or clear credentials fun restartApp(activity: Activity, args: MainActivityArgs) { @@ -82,8 +95,22 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity intent.putExtra(EXTRA_ARGS, args) activity.startActivity(intent) } + + fun getIntentToInitSession(activity: Activity): Intent { + val intent = Intent(activity, MainActivity::class.java) + intent.putExtra(EXTRA_INIT_SESSION, true) + return intent + } + + fun getIntentWithNextIntent(context: Context, nextIntent: Intent): Intent { + val intent = Intent(context, MainActivity::class.java) + intent.putExtra(EXTRA_NEXT_INTENT, nextIntent) + return intent + } } + private val startAppViewModel: StartAppViewModel by viewModel() + override fun getBinding() = ActivityMainBinding.inflate(layoutInflater) override fun getOtherThemes() = ActivityOtherThemes.Launcher @@ -103,15 +130,58 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - args = parseArgs() - if (args.clearCredentials || args.isUserLoggedOut || args.clearCache) { - clearNotifications() + + startAppViewModel.onEach { + renderState(it) } - // Handle some wanted cleanup - if (args.clearCache || args.clearCredentials) { - doCleanUp() + startAppViewModel.viewEvents.stream() + .onEach(::handleViewEvents) + .launchIn(lifecycleScope) + + startAppViewModel.handle(StartAppAction.StartApp) + } + + private fun renderState(state: StartAppViewState) { + if (state.mayBeLongToProcess) { + views.status.setText(R.string.updating_your_data) + } + views.status.isVisible = state.mayBeLongToProcess + } + + private fun handleViewEvents(event: StartAppViewEvent) { + when (event) { + StartAppViewEvent.StartForegroundService -> handleStartForegroundService() + StartAppViewEvent.AppStarted -> handleAppStarted() + } + } + + private fun handleStartForegroundService() { + if (startAppViewModel.shouldStartApp()) { + // Start foreground service, because the operation may take a while + val intent = Intent(this, StartAppAndroidService::class.java) + ContextCompat.startForegroundService(this, intent) + } + } + + private fun handleAppStarted() { + if (intent.hasExtra(EXTRA_NEXT_INTENT)) { + // Start the next Activity + val nextIntent = intent.getParcelableExtra(EXTRA_NEXT_INTENT) + startIntentAndFinish(nextIntent) + } else if (intent.hasExtra(EXTRA_INIT_SESSION)) { + setResult(RESULT_OK) + finish() } else { - startNextActivityAndFinish() + args = parseArgs() + if (args.clearCredentials || args.isUserLoggedOut || args.clearCache) { + clearNotifications() + } + // Handle some wanted cleanup + if (args.clearCache || args.clearCredentials) { + doCleanUp() + } else { + startNextActivityAndFinish() + } } } @@ -241,7 +311,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity // We have a session. // Check it can be opened if (sessionHolder.getActiveSession().isOpenable) { - HomeActivity.newIntent(this, existingSession = true) + HomeActivity.newIntent(this, firstStartMainActivity = false, existingSession = true) } else { // The token is still invalid navigator.softLogout(this) @@ -253,6 +323,10 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity null } } + startIntentAndFinish(intent) + } + + private fun startIntentAndFinish(intent: Intent?) { intent?.let { startActivity(it) } finish() } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 9d7ada9d63..f8a4c5eeca 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -604,7 +604,7 @@ class VectorCallActivity : private fun returnToChat() { val roomId = withState(callViewModel) { it.roomId } val args = TimelineArgs(roomId) - val intent = RoomDetailActivity.newIntent(this, args).apply { + val intent = RoomDetailActivity.newIntent(this, args, false).apply { flags = FLAG_ACTIVITY_CLEAR_TOP } startActivity(intent) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 926c1eb113..ff114e4db9 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -611,6 +611,7 @@ class HomeActivity : companion object { fun newIntent( context: Context, + firstStartMainActivity: Boolean, clearNotification: Boolean = false, authenticationDescription: AuthenticationDescription? = null, existingSession: Boolean = false, @@ -623,10 +624,16 @@ class HomeActivity : inviteNotificationRoomId = inviteNotificationRoomId ) - return Intent(context, HomeActivity::class.java) + val intent = Intent(context, HomeActivity::class.java) .apply { putExtra(Mavericks.KEY_ARG, args) } + + return if (firstStartMainActivity) { + MainActivity.getIntentWithNextIntent(context, intent) + } else { + intent + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt index f1e06dd5ef..a58eed42e1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt @@ -35,6 +35,7 @@ import im.vector.app.core.extensions.keepScreenOn import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityRoomDetailBinding +import im.vector.app.features.MainActivity import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment @@ -191,10 +192,15 @@ class RoomDetailActivity : const val EXTRA_ROOM_ID = "EXTRA_ROOM_ID" const val ACTION_ROOM_DETAILS_FROM_SHORTCUT = "ROOM_DETAILS_FROM_SHORTCUT" - fun newIntent(context: Context, timelineArgs: TimelineArgs): Intent { - return Intent(context, RoomDetailActivity::class.java).apply { + fun newIntent(context: Context, timelineArgs: TimelineArgs, firstStartMainActivity: Boolean): Intent { + val intent = Intent(context, RoomDetailActivity::class.java).apply { putExtra(EXTRA_ROOM_DETAIL_ARGS, timelineArgs) } + return if (firstStartMainActivity) { + MainActivity.getIntentWithNextIntent(context, intent) + } else { + intent + } } // Shortcuts can't have intents with parcelables diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 31c1004ef9..562f2d4aea 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1206,9 +1206,9 @@ class TimelineFragment @Inject constructor( getRootThreadEventId()?.let { val newRoom = timelineArgs.copy(threadTimelineArgs = null, eventId = it) context?.let { con -> - val int = RoomDetailActivity.newIntent(con, newRoom) - int.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK - con.startActivity(int) + val intent = RoomDetailActivity.newIntent(con, newRoom, false) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + con.startActivity(intent) } } } diff --git a/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt index 6de73cb20f..0bdec53f60 100644 --- a/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt @@ -18,18 +18,23 @@ package im.vector.app.features.link import android.content.Intent import android.net.Uri +import android.os.Bundle import androidx.lifecycle.lifecycleScope +import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.toast import im.vector.app.databinding.ActivityProgressBinding +import im.vector.app.features.MainActivity import im.vector.app.features.home.HomeActivity import im.vector.app.features.login.LoginConfig import im.vector.app.features.permalink.PermalinkHandler +import im.vector.app.features.start.StartAppViewModel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.permalinks.PermalinkService import timber.log.Timber @@ -45,12 +50,33 @@ class LinkHandlerActivity : VectorBaseActivity() { @Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var permalinkHandler: PermalinkHandler + private val startAppViewModel: StartAppViewModel by viewModel() + override fun getBinding() = ActivityProgressBinding.inflate(layoutInflater) override fun initUiAndData() { handleIntent() } + private val launcher = registerStartForActivityResult { + if (it.resultCode == RESULT_OK) { + handleIntent() + } else { + // User has pressed back on the MainActivity, so finish also this one. + finish() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (startAppViewModel.shouldStartApp()) { + launcher.launch(MainActivity.getIntentToInitSession(this)) + } else { + handleIntent() + } + } + override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) handleIntent() diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index 4cbebd67a3..763d1eed38 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -221,7 +221,7 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA analyticsScreenName = MobileScreen.ScreenName.Register } val authDescription = inferAuthDescription(loginViewState) - val intent = HomeActivity.newIntent(this, authenticationDescription = authDescription) + val intent = HomeActivity.newIntent(this, firstStartMainActivity = false, authenticationDescription = authDescription) startActivity(intent) finish() return diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 291eee307f..7680b40506 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -173,7 +173,7 @@ class DefaultNavigator @Inject constructor( } val args = TimelineArgs(roomId = roomId, eventId = eventId, isInviteAlreadyAccepted = isInviteAlreadyAccepted) - val intent = RoomDetailActivity.newIntent(context, args) + val intent = RoomDetailActivity.newIntent(context, args, false) startActivity(context, intent, buildTask) } @@ -203,7 +203,7 @@ class DefaultNavigator @Inject constructor( eventId = null, openShareSpaceForId = spaceId.takeIf { postSwitchSpaceAction.showShareSheet } ) - val intent = RoomDetailActivity.newIntent(context, args) + val intent = RoomDetailActivity.newIntent(context, args, false) startActivity(context, intent, false) } } @@ -290,7 +290,7 @@ class DefaultNavigator @Inject constructor( override fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData) { val args = TimelineArgs(roomId, null, sharedData) - val intent = RoomDetailActivity.newIntent(activity, args) + val intent = RoomDetailActivity.newIntent(activity, args, false) activity.startActivity(intent) activity.finish() } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 71c8167788..2948565d58 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -53,6 +53,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.core.services.CallAndroidService import im.vector.app.core.time.Clock import im.vector.app.core.utils.startNotificationChannelSettingsIntent +import im.vector.app.features.MainActivity import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.service.CallHeadsUpActionReceiver import im.vector.app.features.call.webrtc.WebRtcCall @@ -239,9 +240,10 @@ class NotificationUtils @Inject constructor( @SuppressLint("NewApi") fun buildForegroundServiceNotification(@StringRes subTitleResId: Int, withProgress: Boolean = true): Notification { // build the pending intent go to the home screen if this is clicked. - val i = HomeActivity.newIntent(context) + val i = HomeActivity.newIntent(context, firstStartMainActivity = false) i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - val pi = PendingIntent.getActivity(context, 0, i, PendingIntentCompat.FLAG_IMMUTABLE) + val mainIntent = MainActivity.getIntentWithNextIntent(context, i) + val pi = PendingIntent.getActivity(context, 0, mainIntent, PendingIntentCompat.FLAG_IMMUTABLE) val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) @@ -344,7 +346,7 @@ class NotificationUtils @Inject constructor( ) val answerCallPendingIntent = TaskStackBuilder.create(context) - .addNextIntentWithParentStack(HomeActivity.newIntent(context)) + .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false)) .addNextIntent( VectorCallActivity.newIntent( context = context, @@ -468,7 +470,7 @@ class NotificationUtils @Inject constructor( ) val contentPendingIntent = TaskStackBuilder.create(context) - .addNextIntentWithParentStack(HomeActivity.newIntent(context)) + .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false)) .addNextIntent(VectorCallActivity.newIntent(context, call, null)) .getPendingIntent(clock.epochMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) @@ -530,8 +532,8 @@ class NotificationUtils @Inject constructor( .setCategory(NotificationCompat.CATEGORY_CALL) val contentPendingIntent = TaskStackBuilder.create(context) - .addNextIntentWithParentStack(HomeActivity.newIntent(context)) - .addNextIntent(RoomDetailActivity.newIntent(context, TimelineArgs(callInformation.nativeRoomId))) + .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false)) + .addNextIntent(RoomDetailActivity.newIntent(context, TimelineArgs(callInformation.nativeRoomId), true)) .getPendingIntent(clock.epochMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) builder.setContentIntent(contentPendingIntent) @@ -566,6 +568,19 @@ class NotificationUtils @Inject constructor( .build() } + /** + * Creates a notification that indicates the application is initializing. + */ + fun buildStartAppNotification(): Notification { + return NotificationCompat.Builder(context, LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID) + .setContentTitle(stringProvider.getString(R.string.updating_your_data)) + .setSmallIcon(R.drawable.sync) + .setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary)) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build() + } + fun buildDownloadFileNotification(uri: Uri, fileName: String, mimeType: String): Notification { return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) .setGroup(stringProvider.getString(R.string.app_name)) @@ -765,7 +780,11 @@ class NotificationUtils @Inject constructor( joinIntentPendingIntent ) - val contentIntent = HomeActivity.newIntent(context, inviteNotificationRoomId = inviteNotifiableEvent.roomId) + val contentIntent = HomeActivity.newIntent( + context, + firstStartMainActivity = true, + inviteNotificationRoomId = inviteNotifiableEvent.roomId + ) contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that contentIntent.data = createIgnoredUri(inviteNotifiableEvent.eventId) @@ -806,7 +825,7 @@ class NotificationUtils @Inject constructor( .setColor(accentColor) .setAutoCancel(true) .apply { - val contentIntent = HomeActivity.newIntent(context) + val contentIntent = HomeActivity.newIntent(context, firstStartMainActivity = true) contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that contentIntent.data = createIgnoredUri(simpleNotifiableEvent.eventId) @@ -828,14 +847,14 @@ class NotificationUtils @Inject constructor( } private fun buildOpenRoomIntent(roomId: String): PendingIntent? { - val roomIntentTap = RoomDetailActivity.newIntent(context, TimelineArgs(roomId = roomId, switchToParentSpace = true)) + val roomIntentTap = RoomDetailActivity.newIntent(context, TimelineArgs(roomId = roomId, switchToParentSpace = true), true) roomIntentTap.action = TAP_TO_VIEW_ACTION // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that roomIntentTap.data = createIgnoredUri("openRoom?$roomId") // Recreate the back stack return TaskStackBuilder.create(context) - .addNextIntentWithParentStack(HomeActivity.newIntent(context)) + .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false)) .addNextIntent(roomIntentTap) .getPendingIntent( clock.epochMillis().toInt(), @@ -844,13 +863,14 @@ class NotificationUtils @Inject constructor( } private fun buildOpenHomePendingIntentForSummary(): PendingIntent { - val intent = HomeActivity.newIntent(context, clearNotification = true) + val intent = HomeActivity.newIntent(context, firstStartMainActivity = false, clearNotification = true) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP intent.data = createIgnoredUri("tapSummary") + val mainIntent = MainActivity.getIntentWithNextIntent(context, intent) return PendingIntent.getActivity( context, Random.nextInt(1000), - intent, + mainIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt b/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt index 0d7c83e360..7def6d62f0 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/Login2Variant.kt @@ -302,7 +302,8 @@ class Login2Variant( private fun terminate() { val intent = HomeActivity.newIntent( - activity + activity, + firstStartMainActivity = false, ) activity.startActivity(intent) activity.finish() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index bb8c523b5f..867ab45834 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -482,7 +482,11 @@ class FtueAuthVariant( private fun navigateToHome() { withState(onboardingViewModel) { - val intent = HomeActivity.newIntent(activity, authenticationDescription = it.selectedAuthenticationState.description) + val intent = HomeActivity.newIntent( + activity, + firstStartMainActivity = false, + authenticationDescription = it.selectedAuthenticationState.description + ) activity.startActivity(intent) activity.finish() } diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareAction.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareAction.kt index 4e9f024b15..70be2b2b6d 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareAction.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareAction.kt @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary sealed class IncomingShareAction : VectorViewModelAction { data class SelectRoom(val roomSummary: RoomSummary, val enableMultiSelect: Boolean) : IncomingShareAction() object ShareToSelectedRooms : IncomingShareAction() - data class ShareToRoom(val roomSummary: RoomSummary) : IncomingShareAction() + data class ShareToRoom(val roomId: String) : IncomingShareAction() data class ShareMedia(val keepOriginalSize: Boolean) : IncomingShareAction() data class FilterWith(val filter: String) : IncomingShareAction() data class UpdateSharedData(val sharedData: SharedData) : IncomingShareAction() diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt index 439d9b64fa..3d603e3f6a 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareActivity.kt @@ -16,21 +16,66 @@ package im.vector.app.features.share +import android.content.Intent +import android.os.Bundle +import com.airbnb.mvrx.viewModel import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.addFragment +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding +import im.vector.app.features.MainActivity +import im.vector.app.features.start.StartAppViewModel +import javax.inject.Inject @AndroidEntryPoint class IncomingShareActivity : VectorBaseActivity() { + private val startAppViewModel: StartAppViewModel by viewModel() + + @Inject lateinit var activeSessionHolder: ActiveSessionHolder + + private val launcher = registerStartForActivityResult { + if (it.resultCode == RESULT_OK) { + handleAppStarted() + } else { + // User has pressed back on the MainActivity, so finish also this one. + finish() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (startAppViewModel.shouldStartApp()) { + launcher.launch(MainActivity.getIntentToInitSession(this)) + } else { + handleAppStarted() + } + } + override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) override fun getCoordinatorLayout() = views.coordinatorLayout - override fun initUiAndData() { - if (isFirstCreation()) { - addFragment(views.simpleFragmentContainer, IncomingShareFragment::class.java) + private fun handleAppStarted() { + // If we are not logged in, stop the sharing process and open login screen. + // In the future, we might want to relaunch the sharing process after login. + if (!activeSessionHolder.hasActiveSession()) { + startLoginActivity() + } else { + if (isFirstCreation()) { + addFragment(views.simpleFragmentContainer, IncomingShareFragment::class.java) + } } } + + private fun startLoginActivity() { + navigator.openLogin( + context = this, + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK + ) + finish() + } } diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt index 3f8923dd68..3e2ddc469c 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareFragment.kt @@ -30,7 +30,6 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R -import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.registerStartForActivityResult @@ -40,7 +39,6 @@ import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.attachments.ShareIntentHandler import im.vector.app.features.attachments.preview.AttachmentsPreviewActivity import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs -import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import javax.inject.Inject @@ -50,7 +48,6 @@ import javax.inject.Inject */ class IncomingShareFragment @Inject constructor( private val incomingShareController: IncomingShareController, - private val sessionHolder: ActiveSessionHolder, private val shareIntentHandler: ShareIntentHandler, ) : VectorBaseFragment(), @@ -63,12 +60,6 @@ class IncomingShareFragment @Inject constructor( } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - // If we are not logged in, stop the sharing process and open login screen. - // In the future, we might want to relaunch the sharing process after login. - if (!sessionHolder.hasActiveSession()) { - startLoginActivity() - return - } super.onViewCreated(view, savedInstanceState) setupRecyclerView() setupToolbar(views.incomingShareToolbar) @@ -88,7 +79,7 @@ class IncomingShareFragment @Inject constructor( // Direct share if (intent.hasExtra(Intent.EXTRA_SHORTCUT_ID)) { val roomId = intent.getStringExtra(Intent.EXTRA_SHORTCUT_ID)!! - sessionHolder.getSafeActiveSession()?.getRoomSummary(roomId)?.let { viewModel.handle(IncomingShareAction.ShareToRoom(it)) } + viewModel.handle(IncomingShareAction.ShareToRoom(roomId)) } isShareManaged } @@ -192,14 +183,6 @@ class IncomingShareFragment @Inject constructor( .show() } - private fun startLoginActivity() { - navigator.openLogin( - context = requireActivity(), - flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK - ) - requireActivity().finish() - } - override fun invalidate() = withState(viewModel) { views.sendShareButton.isVisible = it.isInMultiSelectionMode incomingShareController.setData(it) diff --git a/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt b/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt index 1191fd04e8..85629ea150 100644 --- a/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/share/IncomingShareViewModel.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.getRoomSummary import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.flow.flow @@ -134,7 +135,8 @@ class IncomingShareViewModel @AssistedInject constructor( private fun handleShareToRoom(action: IncomingShareAction.ShareToRoom) = withState { state -> val sharedData = state.sharedData ?: return@withState - _viewEvents.post(IncomingShareViewEvents.ShareToRoom(action.roomSummary, sharedData, showAlert = false)) + val roomSummary = session.getRoomSummary(action.roomId) ?: return@withState + _viewEvents.post(IncomingShareViewEvents.ShareToRoom(roomSummary, sharedData, showAlert = false)) } private fun handleShareMediaToSelectedRooms(action: IncomingShareAction.ShareMedia) = withState { state -> diff --git a/vector/src/main/java/im/vector/app/features/start/StartAppAction.kt b/vector/src/main/java/im/vector/app/features/start/StartAppAction.kt new file mode 100644 index 0000000000..fffb124f12 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/start/StartAppAction.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.start + +import im.vector.app.core.platform.VectorViewModelAction + +sealed interface StartAppAction : VectorViewModelAction { + object StartApp : StartAppAction +} diff --git a/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt b/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt new file mode 100644 index 0000000000..e8e0eac863 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.start + +import android.content.Intent +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.NamedGlobalScope +import im.vector.app.core.services.VectorAndroidService +import im.vector.app.features.notifications.NotificationUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds + +/** + * A simple foreground service that let the app (and the SDK) time to initialize. + * Will self stop itself once the active session is set. + */ +@AndroidEntryPoint +class StartAppAndroidService : VectorAndroidService() { + + @NamedGlobalScope @Inject lateinit var globalScope: CoroutineScope + @Inject lateinit var notificationUtils: NotificationUtils + @Inject lateinit var activeSessionHolder: ActiveSessionHolder + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + showStickyNotification() + startPollingActiveSession() + return START_STICKY + } + + private fun startPollingActiveSession() { + globalScope.launch { + do { + delay(1.seconds.inWholeMilliseconds) + } while (activeSessionHolder.hasActiveSession().not()) + myStopSelf() + } + } + + private fun showStickyNotification() { + val notificationId = Random.nextInt() + val notification = notificationUtils.buildStartAppNotification() + startForeground(notificationId, notification) + } +} diff --git a/vector/src/main/java/im/vector/app/features/start/StartAppViewEvent.kt b/vector/src/main/java/im/vector/app/features/start/StartAppViewEvent.kt new file mode 100644 index 0000000000..986d41f983 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/start/StartAppViewEvent.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.start + +import im.vector.app.core.platform.VectorViewEvents + +sealed interface StartAppViewEvent : VectorViewEvents { + /** + * Will be sent if the process is taking more than 1 second. + */ + object StartForegroundService : StartAppViewEvent + + /** + * Will be sent when the current Session has been set. + */ + object AppStarted : StartAppViewEvent +} diff --git a/vector/src/main/java/im/vector/app/features/start/StartAppViewModel.kt b/vector/src/main/java/im/vector/app/features/start/StartAppViewModel.kt new file mode 100644 index 0000000000..62a7517f5a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/start/StartAppViewModel.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.start + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.ActiveSessionSetter +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.VectorViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +class StartAppViewModel @AssistedInject constructor( + @Assisted val initialState: StartAppViewState, + private val activeSessionSetter: ActiveSessionSetter, +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: StartAppViewState): StartAppViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + fun shouldStartApp(): Boolean { + return activeSessionSetter.shouldSetActionSession() + } + + override fun handle(action: StartAppAction) { + when (action) { + StartAppAction.StartApp -> handleStartApp() + } + } + + private fun handleStartApp() { + handleLongProcessing() + viewModelScope.launch(Dispatchers.IO) { + // This can take time because of DB migration(s), so do it in a background task. + activeSessionSetter.tryToSetActiveSession(startSync = true) + _viewEvents.post(StartAppViewEvent.AppStarted) + } + } + + private fun handleLongProcessing() { + viewModelScope.launch(Dispatchers.Default) { + delay(1.seconds.inWholeMilliseconds) + setState { copy(mayBeLongToProcess = true) } + _viewEvents.post(StartAppViewEvent.StartForegroundService) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/start/StartAppViewState.kt b/vector/src/main/java/im/vector/app/features/start/StartAppViewState.kt new file mode 100644 index 0000000000..3ff933f054 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/start/StartAppViewState.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.start + +import com.airbnb.mvrx.MavericksState + +data class StartAppViewState( + val mayBeLongToProcess: Boolean = false +) : MavericksState diff --git a/vector/src/main/res/layout/activity_main.xml b/vector/src/main/res/layout/activity_main.xml index c7bca50acb..ba5925f000 100644 --- a/vector/src/main/res/layout/activity_main.xml +++ b/vector/src/main/res/layout/activity_main.xml @@ -1,5 +1,4 @@ - - + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index b699ddae2f..665e1ba8b4 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1625,6 +1625,7 @@ No network. Please check your Internet connection. "Change network" "Please wait…" + Updating your data… "All Communities"