2626import android .content .pm .PackageManager ;
2727import android .os .Environment ;
2828import android .util .Log ;
29+ import org .iiab .controller .update .data .ApkVerifier ;
2930import android .view .View ;
3031import android .widget .ImageButton ;
3132import android .widget .TextView ;
@@ -739,7 +740,7 @@ protected void onResume() {
739740 // Register download listener
740741 IntentFilter filter = new IntentFilter (android .app .DownloadManager .ACTION_DOWNLOAD_COMPLETE );
741742 if (android .os .Build .VERSION .SDK_INT >= android .os .Build .VERSION_CODES .TIRAMISU ) {
742- registerReceiver (downloadReceiver , filter , Context .RECEIVER_EXPORTED );
743+ registerReceiver (downloadReceiver , filter , Context .RECEIVER_NOT_EXPORTED );
743744 } else {
744745 registerReceiver (downloadReceiver , filter );
745746 }
@@ -1296,7 +1297,7 @@ private void startDownload(String downloadUrl) {
12961297
12971298 // 3. We delete previous failed downloads with THIS same name
12981299 java .io .File oldApk = new java .io .File (
1299- android . os . Environment . getExternalStoragePublicDirectory (android .os .Environment .DIRECTORY_DOWNLOADS ),
1300+ getExternalFilesDir (android .os .Environment .DIRECTORY_DOWNLOADS ),
13001301 apkName
13011302 );
13021303 if (oldApk .exists ()) {
@@ -1310,8 +1311,9 @@ private void startDownload(String downloadUrl) {
13101311 request .setMimeType ("application/vnd.android.package-archive" );
13111312 request .setNotificationVisibility (android .app .DownloadManager .Request .VISIBILITY_VISIBLE_NOTIFY_COMPLETED );
13121313
1313- // 4. We tell DownloadManager to use the dynamic name
1314- request .setDestinationInExternalPublicDir (android .os .Environment .DIRECTORY_DOWNLOADS , apkName );
1314+ // 4. F15: stage the APK in the app's PRIVATE external dir (not public
1315+ // Downloads) so another app cannot swap it before install.
1316+ request .setDestinationInExternalFilesDir (this , android .os .Environment .DIRECTORY_DOWNLOADS , apkName );
13151317
13161318 android .app .DownloadManager manager = (android .app .DownloadManager ) getSystemService (Context .DOWNLOAD_SERVICE );
13171319 if (manager != null ) {
@@ -1324,48 +1326,99 @@ private void startDownload(String downloadUrl) {
13241326 @ Override
13251327 public void onReceive (Context context , Intent intent ) {
13261328 long id = intent .getLongExtra (android .app .DownloadManager .EXTRA_DOWNLOAD_ID , -1 );
1327- if (id == updateDownloadId ) {
1329+ if (id != updateDownloadId ) {
1330+ return ;
1331+ }
1332+ // F15: only install if the download actually SUCCEEDED. DownloadManager
1333+ // reports completion even when the server returned an error/HTML page.
1334+ if (isDownloadSuccessful (id )) {
13281335 installApk ();
1336+ } else {
1337+ Log .e (TAG , "OTA: download did not complete successfully; not installing." );
1338+ Toast .makeText (context , R .string .ota_error_download_failed , Toast .LENGTH_LONG ).show ();
13291339 }
13301340 }
13311341 };
13321342
1343+ /** Did the DownloadManager job with this id finish with STATUS_SUCCESSFUL? */
1344+ private boolean isDownloadSuccessful (long id ) {
1345+ android .app .DownloadManager manager =
1346+ (android .app .DownloadManager ) getSystemService (Context .DOWNLOAD_SERVICE );
1347+ if (manager == null ) {
1348+ return false ;
1349+ }
1350+ try (android .database .Cursor c =
1351+ manager .query (new android .app .DownloadManager .Query ().setFilterById (id ))) {
1352+ if (c != null && c .moveToFirst ()) {
1353+ int idx = c .getColumnIndex (android .app .DownloadManager .COLUMN_STATUS );
1354+ return idx >= 0 && c .getInt (idx ) == android .app .DownloadManager .STATUS_SUCCESSFUL ;
1355+ }
1356+ } catch (Exception e ) {
1357+ Log .e (TAG , "OTA: error querying download status" , e );
1358+ }
1359+ return false ;
1360+ }
1361+
13331362 private void installApk () {
13341363 String apkName = getSharedPreferences (getString (R .string .pref_file_internal ), Context .MODE_PRIVATE )
13351364 .getString ("ota_apk_name" , "iiab_update.apk" );
13361365
13371366 java .io .File apkFile = new java .io .File (
1338- android . os . Environment . getExternalStoragePublicDirectory (android .os .Environment .DIRECTORY_DOWNLOADS ),
1367+ getExternalFilesDir (android .os .Environment .DIRECTORY_DOWNLOADS ),
13391368 apkName
13401369 );
13411370
1342- if (apkFile .exists ()) {
1343- Intent intent = new Intent (Intent .ACTION_VIEW );
1344- android .net .Uri apkUri = androidx .core .content .FileProvider .getUriForFile (
1345- this ,
1346- getPackageName () + ".provider" ,
1347- apkFile
1348- );
1349-
1350- intent .setDataAndType (apkUri , "application/vnd.android.package-archive" );
1351- intent .addFlags (Intent .FLAG_GRANT_READ_URI_PERMISSION );
1352- intent .addFlags (Intent .FLAG_ACTIVITY_NEW_TASK );
1353- intent .addFlags (Intent .FLAG_ACTIVITY_CLEAR_TOP );
1371+ if (!apkFile .exists ()) {
1372+ Log .e (TAG , "OTA: Downloaded APK file not found at " + apkFile .getAbsolutePath ());
1373+ return ;
1374+ }
13541375
1355- List <android .content .pm .ResolveInfo > resInfoList = getPackageManager ().queryIntentActivities (intent , PackageManager .MATCH_DEFAULT_ONLY );
1356- for (android .content .pm .ResolveInfo resolveInfo : resInfoList ) {
1357- String packageName = resolveInfo .activityInfo .packageName ;
1358- grantUriPermission (packageName , apkUri , Intent .FLAG_GRANT_READ_URI_PERMISSION );
1359- }
1376+ // F15: verify the APK is signed by the SAME certificate as this app before
1377+ // installing. Rejects MITM/tampered APKs and non-APK downloads (e.g. an
1378+ // HTML/text error page saved with a .apk name).
1379+ if (!ApkVerifier .isSignedBySameCertAsApp (this , apkFile )) {
1380+ Log .e (TAG , "OTA: APK failed signature verification; deleting and aborting install." );
1381+ apkFile .delete ();
1382+ Toast .makeText (this , R .string .ota_error_verify_failed , Toast .LENGTH_LONG ).show ();
1383+ return ;
1384+ }
13601385
1386+ // F15: on API 26+ the user must allow this app to install packages.
1387+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O
1388+ && !getPackageManager ().canRequestPackageInstalls ()) {
1389+ Toast .makeText (this , R .string .ota_msg_enable_unknown_sources , Toast .LENGTH_LONG ).show ();
13611390 try {
1362- startActivity (intent );
1391+ startActivity (new Intent (android .provider .Settings .ACTION_MANAGE_UNKNOWN_APP_SOURCES ,
1392+ android .net .Uri .parse ("package:" + getPackageName ())));
13631393 } catch (Exception e ) {
1364- Log .e (TAG , "OTA: Error launching installer" , e );
1365- Toast .makeText (this , R .string .ota_error_launching_installer , Toast .LENGTH_LONG ).show ();
1394+ Log .e (TAG , "OTA: could not open unknown-sources settings" , e );
13661395 }
1367- } else {
1368- Log .e (TAG , "OTA: Downloaded APK file not found at " + apkFile .getAbsolutePath ());
1396+ return ;
1397+ }
1398+
1399+ Intent intent = new Intent (Intent .ACTION_VIEW );
1400+ android .net .Uri apkUri = androidx .core .content .FileProvider .getUriForFile (
1401+ this ,
1402+ getPackageName () + ".provider" ,
1403+ apkFile
1404+ );
1405+
1406+ intent .setDataAndType (apkUri , "application/vnd.android.package-archive" );
1407+ intent .addFlags (Intent .FLAG_GRANT_READ_URI_PERMISSION );
1408+ intent .addFlags (Intent .FLAG_ACTIVITY_NEW_TASK );
1409+ intent .addFlags (Intent .FLAG_ACTIVITY_CLEAR_TOP );
1410+
1411+ List <android .content .pm .ResolveInfo > resInfoList = getPackageManager ().queryIntentActivities (intent , PackageManager .MATCH_DEFAULT_ONLY );
1412+ for (android .content .pm .ResolveInfo resolveInfo : resInfoList ) {
1413+ String packageName = resolveInfo .activityInfo .packageName ;
1414+ grantUriPermission (packageName , apkUri , Intent .FLAG_GRANT_READ_URI_PERMISSION );
1415+ }
1416+
1417+ try {
1418+ startActivity (intent );
1419+ } catch (Exception e ) {
1420+ Log .e (TAG , "OTA: Error launching installer" , e );
1421+ Toast .makeText (this , R .string .ota_error_launching_installer , Toast .LENGTH_LONG ).show ();
13691422 }
13701423 }
13711424
0 commit comments