1515import android .os .Looper ;
1616import android .os .Message ;
1717import android .util .Log ;
18+ import androidx .annotation .RequiresApi ;
1819import androidx .core .app .ActivityCompat ;
1920import androidx .core .content .ContextCompat ;
2021import androidx .core .content .FileProvider ;
4950import java .util .Arrays ;
5051import java .util .Iterator ;
5152import java .util .Map ;
53+ import java .util .Objects ;
5254
5355/**
5456 * OtaUpdatePlugin
5557 */
56- @ TargetApi (Build .VERSION_CODES .M )
58+ @ RequiresApi (Build .VERSION_CODES .M )
5759public class OtaUpdatePlugin implements FlutterPlugin , ActivityAware , EventChannel .StreamHandler , MethodCallHandler , PluginRegistry .RequestPermissionsResultListener , ProgressListener {
5860
5961 //CONSTANTS
@@ -124,7 +126,7 @@ public void onDetachedFromActivity() {
124126 //METHOD LISTENER
125127 @ Override
126128 public void onMethodCall (MethodCall call , Result result ) {
127- Log .d (TAG , "onMethodCall " + call .method );
129+ Log .d (TAG , "onMethodCall " + call .method );
128130 if (call .method .equals ("getAbi" )) {
129131 result .success (Build .SUPPORTED_ABIS [0 ]);
130132 } else if (call .method .equals ("cancel" )) {
@@ -148,31 +150,28 @@ public void onListen(Object arguments, EventChannel.EventSink events) {
148150 Log .d (TAG , "STREAM OPENED" );
149151 progressSink = events ;
150152 //READ ARGUMENTS FROM CALL
151- Map argumentsMap = ((Map ) arguments );
152- downloadUrl = argumentsMap .get (ARG_URL ).toString ();
153+ //noinspection unchecked
154+ Map <String , String > argumentsMap = ((Map <String , String >) arguments );
155+ downloadUrl = argumentsMap .get (ARG_URL );
153156 try {
154- String headersJson = argumentsMap .get (ARG_HEADERS ). toString () ;
155- if (!headersJson .isEmpty ()) {
157+ String headersJson = argumentsMap .get (ARG_HEADERS );
158+ if (headersJson != null && !headersJson .isEmpty ()) {
156159 headers = new JSONObject (headersJson );
157160 }
158161 } catch (JSONException e ) {
159162 Log .e (TAG , "ERROR: " + e .getMessage (), e );
160163 }
161164 if (argumentsMap .containsKey (ARG_FILENAME ) && argumentsMap .get (ARG_FILENAME ) != null ) {
162- filename = argumentsMap .get (ARG_FILENAME ). toString () ;
165+ filename = argumentsMap .get (ARG_FILENAME );
163166 } else {
164167 filename = DEFAULT_APK_NAME ;
165168 }
166169 if (argumentsMap .containsKey (ARG_CHECKSUM ) && argumentsMap .get (ARG_CHECKSUM ) != null ) {
167- checksum = argumentsMap .get (ARG_CHECKSUM ). toString () ;
170+ checksum = argumentsMap .get (ARG_CHECKSUM );
168171 }
169172 // user-provided provider authority
170- Object authority = ((Map ) arguments ).get (ARG_ANDROID_PROVIDER_AUTHORITY );
171- if (authority != null ) {
172- androidProviderAuthority = authority .toString ();
173- } else {
174- androidProviderAuthority = context .getPackageName () + "." + "ota_update_provider" ;
175- }
173+ String authority = argumentsMap .get (ARG_ANDROID_PROVIDER_AUTHORITY );
174+ androidProviderAuthority = Objects .requireNonNullElseGet (authority , () -> context .getPackageName () + "." + "ota_update_provider" );
176175 executeDownload ();
177176 }
178177
@@ -222,7 +221,7 @@ private void executeDownload() {
222221 if (!file .delete ()) {
223222 Log .e (TAG , "WARNING: unable to delete old apk file before starting OTA" );
224223 }
225- } else if (!file .getParentFile ().exists ()) {
224+ } else if (file . getParentFile () != null && !file .getParentFile ().exists ()) {
226225 if (!file .getParentFile ().mkdirs ()) {
227226 reportError (OtaStatus .INTERNAL_ERROR , "unable to create ota_update folder in internal storage" , null );
228227 }
@@ -255,7 +254,9 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO
255254 }
256255 try {
257256 BufferedSink sink = Okio .buffer (Okio .sink (file ));
258- sink .writeAll (response .body ().source ());
257+ if (response .body () != null ) {
258+ sink .writeAll (response .body ().source ());
259+ }
259260 sink .close ();
260261 } catch (StreamResetException ex ) {
261262 // Thrown when the call was canceled using 'cancel()'
@@ -278,7 +279,7 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO
278279
279280 /**
280281 * Download has been completed
281- *
282+ * <p>
282283 * 1. Check if file exists
283284 * 2. If checksum was provided, compute downloaded file checksum and compare with provided value
284285 * 3. If checks above pass, trigger installation
@@ -293,9 +294,8 @@ private void onDownloadComplete(final String destination, final Uri fileUri) {
293294 reportError (OtaStatus .DOWNLOAD_ERROR , "File was not downloaded" , null );
294295 return ;
295296 }
296-
297297 if (checksum != null ) {
298- //IF user provided checksum verify file integrity
298+ //IF the user provided checksum verify file integrity
299299 try {
300300 if (!Sha256ChecksumValidator .validateChecksum (checksum , downloadedFile )) {
301301 //SEND CHECKSUM ERROR EVENT
@@ -309,23 +309,19 @@ private void onDownloadComplete(final String destination, final Uri fileUri) {
309309 }
310310 }
311311 //TRIGGER APK INSTALLATION
312- handler .post (new Runnable () {
313- @ Override
314- public void run () {
315- executeInstallation (fileUri , downloadedFile );
316- }
317- }
318-
312+ handler .post (() -> executeInstallation (fileUri , downloadedFile )
319313 );
320314 }
321315
322- /**
316+ /**
323317 * Check if app has INSTALL_PACKAGES permission (system app privilege)
324318 */
325319 private boolean hasInstallPackagesPermission () {
326320 try {
327- return context .checkCallingOrSelfPermission ("android.permission.INSTALL_PACKAGES" )
321+ boolean hasInstallPackages = context .checkCallingOrSelfPermission ("android.permission.INSTALL_PACKAGES" )
328322 == PackageManager .PERMISSION_GRANTED ;
323+ Log .d (TAG , "INSTALL_PACKAGES permission: " + hasInstallPackages );
324+ return hasInstallPackages ;
329325 } catch (Exception e ) {
330326 Log .w (TAG , "Error checking INSTALL_PACKAGES permission" , e );
331327 return false ;
@@ -338,39 +334,47 @@ private boolean hasInstallPackagesPermission() {
338334 private void executeSilentInstallation (File downloadedFile ) {
339335 try {
340336 Log .d (TAG , "Attempting silent installation for system app" );
341-
342337 PackageInstaller packageInstaller = context .getPackageManager ().getPackageInstaller ();
338+ // Configure session parameters.
339+ // MODE_FULL_INSTALL means we’re doing a full APK installation (not a staged/delta update).
343340 PackageInstaller .SessionParams params = new PackageInstaller .SessionParams (
344- PackageInstaller .SessionParams .MODE_FULL_INSTALL );
345-
341+ PackageInstaller .SessionParams .MODE_FULL_INSTALL
342+ );
343+ // Create a new installation session and get its unique ID
344+ // Open the session so we can write the APK bytes into it
346345 int sessionId = packageInstaller .createSession (params );
347346 PackageInstaller .Session session = packageInstaller .openSession (sessionId );
348-
349347 try (OutputStream out = session .openWrite ("package" , 0 , -1 );
350- InputStream in = new FileInputStream (downloadedFile )) {
348+ InputStream in = new FileInputStream (downloadedFile )
349+ ) {
350+ // Buffer for copying data from the APK file into the session
351351 byte [] buffer = new byte [65536 ];
352352 int c ;
353353 while ((c = in .read (buffer )) != -1 ) {
354354 out .write (buffer , 0 , c );
355355 }
356356 session .fsync (out );
357357 }
358-
359- // Create an intent for the installation result
358+
359+ // Create intent for the installation result
360360 Intent intent = new Intent (context , OtaUpdatePlugin .class );
361+ // Wrap the result Intent in a PendingIntent, which gives us an IntentSender for commit().
362+ // On Android 12 (S) and above, PendingIntent must be declared mutable/immutable explicitly.
361363 PendingIntent pendingIntent = PendingIntent .getBroadcast (
362364 context ,
363365 sessionId ,
364366 intent ,
365- Build .VERSION .SDK_INT >= Build .VERSION_CODES .S
366- ? PendingIntent .FLAG_MUTABLE
367- : PendingIntent .FLAG_UPDATE_CURRENT );
368-
367+ Build .VERSION .SDK_INT >= Build .VERSION_CODES .S
368+ ? PendingIntent .FLAG_MUTABLE
369+ : PendingIntent .FLAG_UPDATE_CURRENT );
370+
371+ // Commit the session. This hands control over to the system to actually perform the install.
372+ // The provided IntentSender will be invoked with the result of the installation.
369373 session .commit (pendingIntent .getIntentSender ());
370374 session .close ();
371-
375+
372376 Log .d (TAG , "Silent installation session committed" );
373-
377+ // NOTIFY DART PART OF THE PLUGIN, THAT INSTALLATION HAS BEEN SUCCESSFUL
374378 if (progressSink != null ) {
375379 progressSink .success (Arrays .asList ("" + OtaStatus .INSTALLING .ordinal (), "" ));
376380 progressSink .endOfStream ();
@@ -381,9 +385,10 @@ private void executeSilentInstallation(File downloadedFile) {
381385 reportError (OtaStatus .INTERNAL_ERROR , "Silent installation failed: " + e .getMessage (), e );
382386 }
383387 }
388+
384389 /**
385390 * Execute installation
386- *
391+ * <p>
387392 * If app has INSTALL_PACKAGES permission, use silent installation
388393 * For android API level >= 24 start intent for ACTION_INSTALL_PACKAGE (native installer)
389394 * For android API level < 24 start intent ACTION_VIEW (open file, android should prompt for installation)
@@ -392,20 +397,23 @@ private void executeSilentInstallation(File downloadedFile) {
392397 * @param downloadedFile Downloaded file
393398 */
394399 private void executeInstallation (Uri fileUri , File downloadedFile ) {
395- // Try silent installation for system apps first
400+ // Try silent installation for system apps first
396401 if (hasInstallPackagesPermission ()) {
397402 Log .d (TAG , "System app detected, using silent installation" );
398403 executeSilentInstallation (downloadedFile );
399404 return ;
400405 }
401406 Intent intent ;
402- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .N ) {
403- //AUTHORITY NEEDS TO BE THE SAME ALSO IN MANIFEST
404- Uri apkUri = FileProvider .getUriForFile (context , androidProviderAuthority , downloadedFile );
405- intent = new Intent (Intent .ACTION_INSTALL_PACKAGE );
406- intent .setData (apkUri );
407- intent .setFlags (Intent .FLAG_GRANT_READ_URI_PERMISSION )
408- .addFlags (Intent .FLAG_ACTIVITY_NEW_TASK );
407+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .N ) {
408+ Log .d (TAG , "System app detected, using silent installation" );
409+ executeSilentInstallation (downloadedFile );
410+ // //AUTHORITY NEEDS TO BE THE SAME ALSO IN MANIFEST
411+ // Uri apkUri = FileProvider.getUriForFile(context, androidProviderAuthority, downloadedFile);
412+ // intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
413+ // intent.setData(apkUri);
414+ // intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
415+ // .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
416+ return ;
409417 } else {
410418 intent = new Intent (Intent .ACTION_VIEW );
411419 intent .setDataAndType (fileUri , "application/vnd.android.package-archive" );
0 commit comments