mirror of
				https://github.com/recloudstream/cloudstream.git
				synced 2024-08-15 01:53:11 +00:00 
			
		
		
		
	Updated the in app updater to include notifications and updates without user action on Android 12+
This commit is contained in:
		
							parent
							
								
									c11f0c101b
								
							
						
					
					
						commit
						751175b3f9
					
				
					 8 changed files with 354 additions and 23 deletions
				
			
		|  | @ -11,7 +11,10 @@ | |||
|     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide --> | ||||
|     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update --> | ||||
|     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Used for app notifications on Android 13+ --> | ||||
|     <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- not used atm, but code exist that requires it that are not run --> | ||||
|     <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next --> | ||||
|     <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt --> | ||||
|     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service --> | ||||
| 
 | ||||
|     <!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!– Used for getting if vlc is installed –> --> | ||||
|     <!-- Fixes android tv fuckery --> | ||||
|     <uses-feature | ||||
|  | @ -170,6 +173,10 @@ | |||
|             android:name=".ui.ControllerActivity" | ||||
|             android:exported="false" /> | ||||
| 
 | ||||
|         <service | ||||
|             android:name=".utils.PackageInstallerService" | ||||
|             android:exported="false" /> | ||||
| 
 | ||||
|         <provider | ||||
|             android:name="androidx.core.content.FileProvider" | ||||
|             android:authorities="${applicationId}.provider" | ||||
|  |  | |||
|  | @ -88,7 +88,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute | |||
| import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.navigate | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.requestRW | ||||
| import com.lagradost.cloudstream3.utils.USER_PROVIDER_API | ||||
| import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API | ||||
|  |  | |||
|  | @ -128,7 +128,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { | |||
|             ioSafe { | ||||
|                 if (activity?.runAutoUpdate(false) == false) { | ||||
|                     activity?.runOnUiThread { | ||||
|                         CommonActivity.showToast( | ||||
|                         showToast( | ||||
|                             activity, | ||||
|                             R.string.no_update_found, | ||||
|                             Toast.LENGTH_SHORT | ||||
|  |  | |||
|  | @ -7,16 +7,14 @@ import android.net.Uri | |||
| import android.util.Log | ||||
| import android.widget.Toast | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.core.content.FileProvider | ||||
| import androidx.preference.PreferenceManager | ||||
| import com.fasterxml.jackson.annotation.JsonProperty | ||||
| import com.lagradost.cloudstream3.BuildConfig | ||||
| import com.lagradost.cloudstream3.* | ||||
| import com.lagradost.cloudstream3.CommonActivity.showToast | ||||
| import com.lagradost.cloudstream3.R | ||||
| import com.lagradost.cloudstream3.app | ||||
| import com.lagradost.cloudstream3.mvvm.logError | ||||
| import com.lagradost.cloudstream3.utils.AppUtils.parseJson | ||||
| import com.lagradost.cloudstream3.utils.Coroutines.ioSafe | ||||
| import kotlinx.coroutines.sync.Mutex | ||||
| import kotlinx.coroutines.sync.withLock | ||||
| import okio.BufferedSink | ||||
|  | @ -116,6 +114,7 @@ class InAppUpdater { | |||
|                             )?.groupValues?.get(2) | ||||
|                         } | ||||
|                 }).toList().lastOrNull() | ||||
| 
 | ||||
|             val foundAsset = found?.assets?.getOrNull(0) | ||||
|             val currentVersion = packageName?.let { | ||||
|                 packageManager.getPackageInfo( | ||||
|  | @ -245,6 +244,9 @@ class InAppUpdater { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * @param checkAutoUpdate if the update check was launched automatically | ||||
|          **/ | ||||
|         suspend fun Activity.runAutoUpdate(checkAutoUpdate: Boolean = true): Boolean { | ||||
|             val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) | ||||
| 
 | ||||
|  | @ -254,13 +256,20 @@ class InAppUpdater { | |||
|                 ) | ||||
|             ) { | ||||
|                 val update = getAppUpdate() | ||||
|                 if (update.shouldUpdate && update.updateURL != null) { | ||||
|                     //Check if update should be skipped | ||||
|                 if ( | ||||
|                     update.shouldUpdate && | ||||
|                         update.updateURL != null) { | ||||
| 
 | ||||
|                     // Check if update should be skipped | ||||
|                     val updateNodeId = | ||||
|                         settingsManager.getString(getString(R.string.skip_update_key), "") | ||||
|                     if (update.updateNodeId.equals(updateNodeId)) { | ||||
| 
 | ||||
|                     // Skips the update if its an automatic update and the update is skipped | ||||
|                     // This allows updating manually | ||||
|                     if (update.updateNodeId.equals(updateNodeId) && checkAutoUpdate) { | ||||
|                         return false | ||||
|                     } | ||||
| 
 | ||||
|                     runOnUiThread { | ||||
|                         try { | ||||
|                             val currentVersion = packageName?.let { | ||||
|  | @ -283,16 +292,23 @@ class InAppUpdater { | |||
|                             builder.apply { | ||||
|                                 setPositiveButton(R.string.update) { _, _ -> | ||||
|                                     showToast(context, R.string.download_started, Toast.LENGTH_LONG) | ||||
|                                     ioSafe { | ||||
|                                         if (!downloadUpdate(update.updateURL)) | ||||
|                                             runOnUiThread { | ||||
|                                                 showToast( | ||||
|                                                     context, | ||||
|                                                     R.string.download_failed, | ||||
|                                                     Toast.LENGTH_LONG | ||||
|                                                 ) | ||||
|                                             } | ||||
|                                     } | ||||
|                                     val intent = PackageInstallerService.getIntent( | ||||
|                                         context, | ||||
|                                         update.updateURL | ||||
|                                     ) | ||||
|                                     ContextCompat.startForegroundService(context, intent) | ||||
| //                                    ioSafe { | ||||
| //                                        if ( | ||||
| //                                        !downloadUpdate(update.updateURL) | ||||
| //                                        ) | ||||
| //                                            runOnUiThread { | ||||
| //                                                showToast( | ||||
| //                                                    context, | ||||
| //                                                    R.string.download_failed, | ||||
| //                                                    Toast.LENGTH_LONG | ||||
| //                                                ) | ||||
| //                                            } | ||||
| //                                    } | ||||
|                                 } | ||||
| 
 | ||||
|                                 setNegativeButton(R.string.cancel) { _, _ -> } | ||||
|  | @ -302,8 +318,7 @@ class InAppUpdater { | |||
|                                         settingsManager.edit().putString( | ||||
|                                             getString(R.string.skip_update_key), | ||||
|                                             update.updateNodeId ?: "" | ||||
|                                         ) | ||||
|                                             .apply() | ||||
|                                         ).apply() | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|  |  | |||
|  | @ -0,0 +1,106 @@ | |||
| package com.lagradost.cloudstream3.utils | ||||
| 
 | ||||
| import android.app.PendingIntent | ||||
| import android.content.BroadcastReceiver | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.content.IntentFilter | ||||
| import android.content.pm.PackageInstaller | ||||
| import android.os.Build | ||||
| import com.lagradost.cloudstream3.mvvm.logError | ||||
| import java.io.InputStream | ||||
| 
 | ||||
| const val INSTALL_ACTION = "ApkInstaller.INSTALL_ACTION" | ||||
| 
 | ||||
| 
 | ||||
| class ApkInstaller(private val service: PackageInstallerService) { | ||||
|     private val packageInstaller = service.packageManager.packageInstaller | ||||
| 
 | ||||
|     enum class InstallProgressStatus { | ||||
|         Preparing, | ||||
|         Downloading, | ||||
|         Installing, | ||||
|         Failed, | ||||
|     } | ||||
| 
 | ||||
|     private val installActionReceiver = object : BroadcastReceiver() { | ||||
|         override fun onReceive(context: Context, intent: Intent) { | ||||
|             when (intent.getIntExtra( | ||||
|                 PackageInstaller.EXTRA_STATUS, | ||||
|                 PackageInstaller.STATUS_FAILURE | ||||
|             )) { | ||||
|                 PackageInstaller.STATUS_PENDING_USER_ACTION -> { | ||||
|                     val userAction = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT) | ||||
|                     userAction?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | ||||
|                     context.startActivity(userAction) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun installApk( | ||||
|         context: Context, | ||||
|         inputStream: InputStream, | ||||
|         size: Long, | ||||
|         installProgress: (bytesRead: Int) -> Unit, | ||||
|         installProgressStatus: (InstallProgressStatus) -> Unit | ||||
|     ) { | ||||
|         installProgressStatus.invoke(InstallProgressStatus.Preparing) | ||||
|         var activeSession: Int? = null | ||||
| 
 | ||||
|         try { | ||||
|             val installParams = | ||||
|                 PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) | ||||
| 
 | ||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||||
|                 installParams.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED) | ||||
|             } | ||||
| 
 | ||||
|             activeSession = packageInstaller.createSession(installParams) | ||||
|             installParams.setSize(size) | ||||
| 
 | ||||
|             val session = packageInstaller.openSession(activeSession) | ||||
|             installProgressStatus.invoke(InstallProgressStatus.Downloading) | ||||
| 
 | ||||
|             session.openWrite(context.packageName, 0, size) | ||||
|                 .use { outputStream -> | ||||
|                     val buffer = ByteArray(1024) | ||||
|                     var bytesRead = inputStream.read(buffer) | ||||
| 
 | ||||
|                     while (bytesRead >= 0) { | ||||
|                         outputStream.write(buffer, 0, bytesRead) | ||||
|                         bytesRead = inputStream.read(buffer) | ||||
|                         installProgress.invoke(bytesRead) | ||||
|                     } | ||||
| 
 | ||||
|                     inputStream.close() | ||||
|                 } | ||||
| 
 | ||||
|             installProgressStatus.invoke(InstallProgressStatus.Installing) | ||||
| 
 | ||||
|             val intentSender = PendingIntent.getBroadcast( | ||||
|                 service, | ||||
|                 activeSession, | ||||
|                 Intent(INSTALL_ACTION), | ||||
|                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0, | ||||
|             ).intentSender | ||||
| 
 | ||||
|             session.commit(intentSender) | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
| 
 | ||||
|             service.unregisterReceiver(installActionReceiver) | ||||
|             installProgressStatus.invoke(InstallProgressStatus.Failed) | ||||
| 
 | ||||
|             activeSession?.let { sessionId -> | ||||
|                 packageInstaller.abandonSession(sessionId) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     init { | ||||
|         service.registerReceiver(installActionReceiver, IntentFilter(INSTALL_ACTION)) | ||||
|         service.receivers.add(installActionReceiver) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -0,0 +1,200 @@ | |||
| package com.lagradost.cloudstream3.utils | ||||
| 
 | ||||
| import android.app.NotificationChannel | ||||
| import android.app.NotificationManager | ||||
| import android.app.PendingIntent | ||||
| import android.app.Service | ||||
| import android.content.BroadcastReceiver | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.os.Build | ||||
| import android.os.IBinder | ||||
| import android.util.Log | ||||
| import androidx.core.app.NotificationCompat | ||||
| import com.lagradost.cloudstream3.MainActivity | ||||
| import com.lagradost.cloudstream3.R | ||||
| import com.lagradost.cloudstream3.app | ||||
| import com.lagradost.cloudstream3.utils.Coroutines.ioSafe | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlinx.coroutines.sync.Mutex | ||||
| import kotlinx.coroutines.sync.withLock | ||||
| import kotlin.math.roundToInt | ||||
| 
 | ||||
| 
 | ||||
| class PackageInstallerService : Service() { | ||||
|     val receivers = mutableListOf<BroadcastReceiver>() | ||||
| 
 | ||||
|     private val baseNotification by lazy { | ||||
|         val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||||
|             PendingIntent.FLAG_MUTABLE | ||||
|         } else 0 | ||||
| 
 | ||||
|         val intent = Intent(this, MainActivity::class.java) | ||||
|         val pendingIntent = | ||||
|             PendingIntent.getActivity(this, 0, intent, flag) | ||||
| 
 | ||||
|         NotificationCompat.Builder(this, UPDATE_CHANNEL_ID) | ||||
|             .setAutoCancel(false) | ||||
|             .setColorized(true) | ||||
|             .setOnlyAlertOnce(true) | ||||
|             .setSilent(true) | ||||
|             // If low priority then the notification might not show :( | ||||
|             .setPriority(NotificationCompat.PRIORITY_DEFAULT) | ||||
|             .setColor(this.colorFromAttribute(R.attr.colorPrimary)) | ||||
|             .setContentTitle(getString(R.string.update_notification_downloading)) | ||||
|             .setContentIntent(pendingIntent) | ||||
|             .setSmallIcon(R.drawable.rdload) | ||||
|     } | ||||
| 
 | ||||
|     private fun createNotificationChannel() { | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|             val importance = NotificationManager.IMPORTANCE_DEFAULT | ||||
|             val channel = | ||||
|                 NotificationChannel(UPDATE_CHANNEL_ID, UPDATE_CHANNEL_NAME, importance).apply { | ||||
|                     description = UPDATE_CHANNEL_DESCRIPTION | ||||
|                 } | ||||
| 
 | ||||
|             // Register the channel with the system | ||||
|             val notificationManager: NotificationManager = | ||||
|                 this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | ||||
| 
 | ||||
|             notificationManager.createNotificationChannel(channel) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreate() { | ||||
|         createNotificationChannel() | ||||
|         startForeground(UPDATE_NOTIFICATION_ID, baseNotification.build()) | ||||
|     } | ||||
| 
 | ||||
|     private val updateLock = Mutex() | ||||
| 
 | ||||
|     private suspend fun downloadUpdate(url: String): Boolean { | ||||
|         try { | ||||
|             Log.d(LOG_TAG, "Downloading update: $url") | ||||
| 
 | ||||
|             // Delete all old updates | ||||
|             ioSafe { | ||||
|                 val appUpdateName = "CloudStream" | ||||
|                 val appUpdateSuffix = "apk" | ||||
| 
 | ||||
|                 this@PackageInstallerService.cacheDir.listFiles()?.filter { | ||||
|                     it.name.startsWith(appUpdateName) && it.extension == appUpdateSuffix | ||||
|                 }?.forEach { | ||||
|                     it.deleteOnExit() | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             updateLock.withLock { | ||||
|                 updateNotificationProgress( | ||||
|                     0f, | ||||
|                     ApkInstaller.InstallProgressStatus.Downloading | ||||
|                 ) | ||||
| 
 | ||||
|                 val body = app.get(url).body | ||||
|                 val inputStream = body.byteStream() | ||||
|                 val installer = ApkInstaller(this) | ||||
|                 val totalSize = body.contentLength() | ||||
|                 var currentSize = 0 | ||||
| 
 | ||||
|                 installer.installApk(this, inputStream, totalSize, { | ||||
|                     currentSize += it | ||||
|                     // Prevent div 0 | ||||
|                     if (totalSize == 0L) return@installApk | ||||
| 
 | ||||
|                     val percentage = currentSize / totalSize.toFloat() | ||||
|                     updateNotificationProgress( | ||||
|                         percentage, | ||||
|                         ApkInstaller.InstallProgressStatus.Downloading | ||||
|                     ) | ||||
|                 }) { status -> | ||||
|                     updateNotificationProgress(0f, status) | ||||
|                 } | ||||
|             } | ||||
|             return true | ||||
|         } catch (e: Exception) { | ||||
|             updateNotificationProgress(0f, ApkInstaller.InstallProgressStatus.Failed) | ||||
|             return false | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun updateNotificationProgress( | ||||
|         percentage: Float, | ||||
|         state: ApkInstaller.InstallProgressStatus | ||||
|     ) { | ||||
| //        Log.d(LOG_TAG, "Downloading app update progress $percentage | $state") | ||||
|         val text = when (state) { | ||||
|             ApkInstaller.InstallProgressStatus.Installing -> R.string.update_notification_installing | ||||
|             ApkInstaller.InstallProgressStatus.Preparing, ApkInstaller.InstallProgressStatus.Downloading -> R.string.update_notification_downloading | ||||
|             ApkInstaller.InstallProgressStatus.Failed -> R.string.update_notification_failed | ||||
|         } | ||||
| 
 | ||||
|         val newNotification = baseNotification | ||||
|             .setContentTitle(getString(text)) | ||||
|             .apply { | ||||
|                 if (state == ApkInstaller.InstallProgressStatus.Failed) { | ||||
|                     setSmallIcon(R.drawable.rderror) | ||||
|                     setAutoCancel(true) | ||||
|                 } else { | ||||
|                     setProgress( | ||||
|                         10000, (10000 * percentage).roundToInt(), | ||||
|                         state != ApkInstaller.InstallProgressStatus.Downloading | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|             .build() | ||||
| 
 | ||||
|         val notificationManager = | ||||
|             getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | ||||
| 
 | ||||
|         // Persistent notification on failure | ||||
|         val id = | ||||
|             if (state == ApkInstaller.InstallProgressStatus.Failed) UPDATE_NOTIFICATION_ID + 1 else UPDATE_NOTIFICATION_ID | ||||
|         notificationManager.notify(id, newNotification) | ||||
|     } | ||||
| 
 | ||||
|     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | ||||
|         val url = intent?.getStringExtra(EXTRA_URL) ?: return START_NOT_STICKY | ||||
|         ioSafe { | ||||
|             downloadUpdate(url) | ||||
|             // Close the service after the update is done | ||||
|             // If no sleep then the install prompt may not appear and the notification | ||||
|             // will disappear instantly | ||||
|             delay(10_000) | ||||
|             this@PackageInstallerService.stopSelf() | ||||
|         } | ||||
|         return START_NOT_STICKY | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroy() { | ||||
|         receivers.forEach { | ||||
|             try { | ||||
|                 this.unregisterReceiver(it) | ||||
|             } catch (_: IllegalArgumentException) { | ||||
|                 // Receiver not registered | ||||
|             } | ||||
|         } | ||||
|         super.onDestroy() | ||||
|     } | ||||
| 
 | ||||
|     override fun onBind(i: Intent?): IBinder? = null | ||||
| 
 | ||||
|     companion object { | ||||
|         private const val EXTRA_URL = "EXTRA_URL" | ||||
|         private const val LOG_TAG = "PackageInstallerService" | ||||
| 
 | ||||
|         const val UPDATE_CHANNEL_ID = "cloudstream3.updates" | ||||
|         const val UPDATE_CHANNEL_NAME = "App Updates" | ||||
|         const val UPDATE_CHANNEL_DESCRIPTION = "App updates notification channel" | ||||
|         const val UPDATE_NOTIFICATION_ID = -68454136 | ||||
| 
 | ||||
|         fun getIntent( | ||||
|             context: Context, | ||||
|             url: String, | ||||
|         ): Intent { | ||||
|             return Intent(context, PackageInstallerService::class.java) | ||||
|                 .putExtra(EXTRA_URL, url) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -659,4 +659,8 @@ | |||
|     <string name="confirm_exit_dialog">Are you sure you want to exit?</string> | ||||
|     <string name="yes">Yes</string> | ||||
|     <string name="no">No</string> | ||||
| 
 | ||||
|     <string name="update_notification_downloading">Downloading app update</string> | ||||
|     <string name="update_notification_installing">Installing app update</string> | ||||
|     <string name="update_notification_failed">App update failed</string> | ||||
| </resources> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue