1

I'm making an APK file download screen and i show progress which is the percent of the file downloaded in the screen and also i have a notification to use a Foreground service because the file download must be downloaded from a service. During downloading, users can leave the app and can go back to my app through the notification. But the problem is, to achieve this logic, i use PendingIntent and when user click my app's notification, it recreates the app instead of going back to the previous screen earlier. I don't know why. please check my codes below.

File Download Fragment

 class FileDownloadFragment(private val uri: String) : DialogFragment() {

 ...

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
     super.onViewCreated(view, savedInstanceState)
     startDownloadService()
 }

 private fun startDownloadService() {
     val downloadService = AppUpdateAPKDownloadService()
     val startIntent = Intent(context, downloadService::class.java)
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
         context.startForegroundService(startIntent)
     } else {
         context.startService(startIntent)
     }
 }

 ...

}

Foreground Service

  class AppUpdateAPKDownloadService: LifecycleService() {

  ...

  /** Dispatcher */
  private val ioDispatcher = Dispatchers.IO

  override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
      super.onStartCommand(intent, flags, startId)
      if (intent != null) {
          createNotificationChannel()
          val notification = getNotification()
          startForeground(NOTIFICATION_ID, notification)

          lifecycleScope.launch{
              downloadAPK(intent.getStringExtra(UPDATE_APK_URI).toString())
          }
      }

      return START_NOT_STICKY
  }

  private fun createNotificationChannel() {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
          val channel = NotificationChannel(
              CHANNEL_ID,
               CHANNEL_NAME,
              NotificationManager.IMPORTANCE_DEFAULT
          ).apply {
              enableVibration(true)
              enableLights(true)
          }
          val manager = this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        manager.createNotificationChannel(channel)
      }
  }

  private fun getNotification(): Notification {
      val intent = Intent(applicationContext, SplashActivity::class.java)
      intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
      val pendingIntent = TaskStackBuilder.create(applicationContext).run {
          addNextIntentWithParentStack(intent)
          getPendingIntent(APP_UPDATE_PENDING_INTENT_REQUEST_CODE, PendingIntent.FLAG_IMMUTABLE)
      }

      // val pendingIntent = PendingIntent.getActivity(
      //     applicationContext,
      //     APP_UPDATE_PENDING_INTENT_REQUEST_CODE,
      //     intent,
      //     PendingIntent.FLAG_IMMUTABLE
      //     )

      val notification = NotificationCompat.Builder(this, CHANNEL_ID).apply {
          setContentTitle("DownloadApp")
          setContentText("Downloading...")
          setSmallIcon(R.drawable.icon)
       setLargeIcon(BitmapFactory.decodeResource(this@AppUpdateAPKDownloadService.applicationContext.resources, R.mipmap.icon))
          priority = NotificationCompat.PRIORITY_HIGH
          setContentIntent(pendingIntent)
          setOngoing(true)
          setAutoCancel(true)
       }.build()

      val notificationManager = NotificationManagerCompat.from(this)
      notificationManager.notify(NOTIFICATION_ID, notification)

      return notification
  }

  private suspend fun downloadAPK(uri: String) = withContext(ioDispatcher) {
      kotlin.runCatching {
          URL(uri)
      }.onSuccess { url ->
          val connection: URLConnection = url.openConnection()
          connection.connect()

          val fileFullSize = connection.contentLength

          val directory = applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
          directory?.let {
              if (!directory.exists()) directory.mkdirs()
          }

          val inputDataStream = DataInputStream(BufferedInputStream(url.openStream(), fileFullSize))
          val file = File(directory, "my_app.apk")
          val outputDataStream = DataOutputStream(BufferedOutputStream(FileOutputStream(file), fileFullSize))
          processDownload(
              inputStream = inputDataStream,
              outputStream = outputDataStream,
              fileFullSize = fileFullSize
          )

          val bundle = Bundle().apply {
              putInt(UPDATE_APK_RECEIVER_MODE, UPDATE_APK_DOWNLOAD_COMPLETE)
          }
          uiUpdateReceiver?.send(Activity.RESULT_OK, bundle)
      }.onFailure {
         
      }
  }

  private fun processDownload(
      inputStream: DataInputStream,
      outputStream: DataOutputStream,
      fileFullSize: Int
  ) {
      val data = ByteArray(fileFullSize)
      var downloadSize: Long = 0
      var count: Int

      while (inputStream.read(data).also { count = it } != -1) {
          downloadSize += count
          val percent = (downloadSize.toFloat() / fileFullSize) * 100
          Log.d("TEST", "writing...$percent% in Service")
          val bundle = Bundle().apply {
              putInt(UPDATE_APK_RECEIVER_MODE, UPDATE_APK_UI_UPDATE)
              putFloat(UPDATE_API_UI_PERCENT, percent)
          }
          uiUpdateReceiver?.send(Activity.RESULT_OK, bundle)
          outputStream.write(data, 0, count)
      }

      outputStream.flush()
      outputStream.close()
      inputStream.close()
  }

  ...
}



I also add android:launchMode="singleTop" to SplashActivity in AndroidManifest.xml but it's still not working...
What mistake did I make?

CodingBruceLee
  • 657
  • 1
  • 5
  • 19

2 Answers2

4

TaskStackBuilder will always recreate the activities. That's the way it is designed. You don't want to use it if you want to return to an existing task.

Instead of your code to create the PendingIntent, use this:

val intent = PackageManager.getLaunchIntentForPackage("your.package.name")
val pendingIntent = PendingIntent.getActivity(applicationContext,
            APP_UPDATE_PENDING_INTENT_REQUEST_CODE,
            intent,
            PendingIntent.FLAG_IMMUTABLE)

This will just cause your existing task to be brought to the foreground in whatever state it was in last. If your app is not running, this will launch your app as it would if you tapped the app icon on the HOME screen.

David Wasser
  • 93,459
  • 16
  • 209
  • 274
  • Hi David, It's a problem. your solution only works on Android 12 but doesn't work on less than Android 12. it creates a new Activity.. i do the code in a Service component like this val intent = applicationContext.packageManager.getLaunchIntentForPackage(applicationContext.packageName) val pendingIntent = PendingIntent.getActivity(applicationContext, APP_UPDATE_PENDING_INTENT_REQUEST_CODE, intent, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.FLAG_IMMUTABLE } else { PendingIntent.FLAG_UPDATE_CURRENT } ) – CodingBruceLee May 11 '22 at 00:54
  • You can pass FLAG_IMMUTABLE to any version of Android. On Android < 12 it is simply ignored. – David Wasser May 11 '22 at 08:53
  • Also, you don't need `FLAG_UPDATE_CURRENT` in any case. – David Wasser May 11 '22 at 08:54
  • Thank you for letting me know but the point is, when i use `applicationContext.packageManager.getLaunchIntentForPackage(applicationContext.packageName)`, it recreates a new activity... it's only working on Android12. – CodingBruceLee May 11 '22 at 09:01
  • That is strange and should not happen. When you reproduce this, how do you launch the app **for the first time**? From Android Studio? Or from the Installer? Or by clicking on the HOME-screen icon? – David Wasser May 11 '22 at 09:28
  • I launched from Android Studio for all of Android versions. is it different to launch between Android Studio or Installer or Home-screen icon?? – CodingBruceLee May 12 '22 at 00:13
  • I added `android:launchMode="singleTop"` again in `AndroidManifest.xml` with `packageManager.getLaunchIntentForPackage(applicationContext.packageName)` because `setFlag(Intent.FLAG_ACTIVITY_SINGLE_TOP)` didn't work programmatically. it's working on all of Android versions. in my case, i feel like it's fine because there is no any other activities on that screen where i left. but i wonder you mean `android:luanchMode="singleTop" is unnecessary to just go back to the screen where i left. – CodingBruceLee May 12 '22 at 00:56
  • If you are launching from Android Studio, then you are probably seeing this nasty long-standing Android bug: https://stackoverflow.com/questions/16283079/re-launch-of-activity-on-home-button-but-only-the-first-time/16447508#16447508 To check that, after you install or launch your app from Android Studio, go to Settings->Apps->YourApp and force stop the app (kill it). Then start the app by tapping the icon on the HOME screen and see if it still creates a second copy of your `Activity`. – David Wasser May 12 '22 at 09:05
  • 1
    Thank you David. It was really helpful to me. and i had to use `isTaskRoot()` in `onCreate()`. but in Andorid 11 and 12, i didn't need to use `isTaskRoot()` in `onCreate()` anymore. It worked well without it. – CodingBruceLee May 16 '22 at 01:59
  • 1
    Good to know. I guess they finally fixed this in Android 11 then. Took only about 10 years :-( – David Wasser May 16 '22 at 12:06
0

Okay, I solved this problem. it's 3 steps.

  1. use PendingIntent.getActivity() instead of TaskStackBuilder.create(applicationContext).

  2. use PendingIntent.FLAG_IMMUTABLE if you target Android M(API23) or above, or PendingIntent.FLAG_UPDATE_CURRENT if you target Android Lollipop(API21) or below.

  3. add these flags Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP to the Intent.

CodingBruceLee
  • 657
  • 1
  • 5
  • 19
  • 1
    `TaskStackBuilder` will always recreate the activities. That's the way it is designed. You don't want to use it if you want to return to an existing task. – David Wasser May 09 '22 at 10:53
  • 1
    Using `CLEAR_TOP or SINGLE_TOP` will cause any activities that are on top of the target `Activity` to be finished. That may or may not be what you want. If you want to just bring your app to the foreground in whatever state it is in, you are better off using `PackageManager.getLaunchIntentForPackage()` to create an `Intent` that you can then wrap in a `PendingIntent`. – David Wasser May 09 '22 at 10:55
  • 1
    @DavidWasser Hi, David! I'm really appreciated for your workaround! it worked and it's a better way than mine. Thank you! – CodingBruceLee May 10 '22 at 00:33
  • OK, I added an answer for you. – David Wasser May 10 '22 at 08:21