6

Hello StackOverflow users,

i have an Android app outside of the Play Store. It updates itself by downloading a new APK and invoking the installer dialog using an Intent. The update functionality does not work anymore on Android 10.

I need to use the PackageInstaller API on Android 10 now, but i can't get it to work. My app is not a device or profile owner, but since i don't want a silent install so i think it should be fine.

My problem is that as soon as i commit the session absolutely nothing happens.

PackageInstaller installer = activity.PackageManager.PackageInstaller;
PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(PackageInstallMode.FullInstall);
int sessionId = installer.CreateSession(sessionParams);
PackageInstaller.Session session = installer.OpenSession(sessionId);

var input = new FileStream(pfad, FileMode.Open, FileAccess.Read);
var packageInSession = session.OpenWrite("package", 0, -1);
input.CopyTo(packageInSession);
packageInSession.Close();
input.Close();

//That this is necessary could be a Xamarin bug.
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Intent intent = new Intent(activity, activity.Class);
intent.SetAction("com.example.android.apis.content.SESSION_API_PACKAGE_INSTALLED");
PendingIntent pendingIntent = PendingIntent.GetActivity(activity, 0, intent, 0);
IntentSender statusReceiver = pendingIntent.IntentSender;

// Commit the session (this will start the installation workflow).
session.Commit(statusReceiver);

I took a look at the DDMS and got nothing relevant out of it. One thing that might be of interest is that when i Dispose() the streams, i get an IOException: write failed (EBADF) bad file descriptor which would indicate a bad APK. But i doubt it's that because i can install the APK using a file manager without a hitch. Googling the error didn't lead me anywhere.

How can i fix this problem?

AlphaNERD
  • 155
  • 1
  • 1
  • 9
  • First of all, please refer to this asnwer of thread https://stackoverflow.com/questions/58374527/installing-apk-that-updates-the-same-app-fails-on-android-10-java-lang-security,this exception is related to the stream used, but it cannot be closed. Please check your application, if closed all of the stream. – Leon Jan 13 '20 at 06:14
  • @LeonLu-MSFT I didn't keep the streams open. The EBADF error happens when i dispose (not close) the stream. – AlphaNERD Jan 13 '20 at 08:32
  • Write the bytes of the APK (read from an InputStream from the Uri) to an OutputStream supplied by that session like above link And please Call close() to close up the session. – Leon Jan 14 '20 at 08:51
  • @LeonLu-MSFT The method CopyTo already copies all the bytes to the target stream. And closing the session didn't make a difference. – AlphaNERD Jan 14 '20 at 10:01
  • @AlphaNERD did you find a solution to the EBADF issue? – smedasn Mar 25 '20 at 15:29
  • I have a working Xamarin installer here - did not run into GC issues: https://stackoverflow.com/a/69774124/1399272 – Bondolin Oct 29 '21 at 20:15

1 Answers1

7

There are a couple of things you need to make sure in order for the apk installation to succeed in Android Q:

  • Do not use using statements or try to dispose anything inside the AddApkToInstallSession method. The Dispose causes the installation to fail. Use try/finally and close instead:

private static void AddApkToInstallSession(Context context, Android.Net.Uri apkUri, PackageInstaller.Session session)
{
  var packageInSession = session.OpenWrite("package", 0, -1);
  var input = context.ContentResolver.OpenInputStream(apkUri);

  try
  {
      if (input != null)
      {
          input.CopyTo(packageInSession);
      }
      else
      {
          throw new Exception("Inputstream is null");
      }
  }
  finally
  {
      packageInSession.Close();
      input.Close();
  }

  //That this is necessary could be a Xamarin bug.
  GC.Collect();
  GC.WaitForPendingFinalizers();
  GC.Collect();
}
  • You must override the "OnNewIntent" method because you need an intent to confirm the installation of the APK file:

protected override void OnNewIntent(Intent intent)
{
    Bundle extras = intent.Extras;
    if (PACKAGE_INSTALLED_ACTION.Equals(intent.Action))
    {
        var status = extras.GetInt(PackageInstaller.ExtraStatus);
        var message = extras.GetString(PackageInstaller.ExtraStatusMessage);
        switch (status)
        {
            case (int)PackageInstallStatus.PendingUserAction:
                // Ask user to confirm the installation
                var confirmIntent = (Intent)extras.Get(Intent.ExtraIntent);
                StartActivity(confirmIntent);
                break;
            case (int)PackageInstallStatus.Success:
                //TODO: Handle success
                break;
            case (int)PackageInstallStatus.Failure:
            case (int)PackageInstallStatus.FailureAborted:
            case (int)PackageInstallStatus.FailureBlocked:
            case (int)PackageInstallStatus.FailureConflict:
            case (int)PackageInstallStatus.FailureIncompatible:
            case (int)PackageInstallStatus.FailureInvalid:
            case (int)PackageInstallStatus.FailureStorage:
                //TODO: Handle failures
                break;
        }
    }
}
  • The Activity where you override the "OnNewIntent" method must have LaunchMode set to LaunchMode.SingleTop
  • The user must have given the application from which you try to install the APK file the necessary permissions to install APK's. You can check if this is the case by calling PackageManager.CanRequestPackageInstalls(). If this function returns false, you can open the application options window by using this code:
StartActivity(new Intent(
            Android.Provider.Settings.ActionManageUnknownAppSources,
            Android.Net.Uri.Parse("package:" + Android.App.Application.Context.PackageName)));

so the user can easily set the switch to enable this.

  • This is my main method to initialize the APK installation:

public void InstallPackageAndroidQAndAbove(Android.Net.Uri apkUri)
{
    var packageInstaller = PackageManager.PackageInstaller;
    var sessionParams = new PackageInstaller.SessionParams(PackageInstallMode.FullInstall);
    int sessionId = packageInstaller.CreateSession(sessionParams);
    var session = packageInstaller.OpenSession(sessionId);

    AddApkToInstallSession(apkUri, session);

    // Create an install status receiver.
    var intent = new Intent(this, this.Class);
    intent.SetAction(PACKAGE_INSTALLED_ACTION);
    var pendingIntent = PendingIntent.GetActivity(this, 0, intent, 0);
    var statusReceiver = pendingIntent.IntentSender;

    // Commit the session (this will start the installation workflow).
    session.Commit(statusReceiver);
}
  • If you are debugging on a Xiaomi device, you must disable MIUI Optimizations under developer options. Otherwise the installation will fail with permission denied error.
brz
  • 1,846
  • 21
  • 21
  • I'm try same thing with xamarin on android 10. I follow your guideline, no error, but intent never call. If I force calll start activity activity.StartActivityForResult(intent, 0); I get : {Android.Content.ActivityNotFoundException: Unable to find explicit activity class {ch.....logymobile/android.app.Application}; have you declared this activity in your AndroidManifest.xml? Anything I missed on the manifest or ? – Olivier Nov 30 '20 at 12:39
  • I see you are using the undeclared var PACKAGE_INSTALLED_ACTION as if it where a const, so I understand you are simply copying code from another entries (there are lots of entries copying that non-working code). That const is declared as PACKAGE_INSTALLED_ACTION = "com.example.android.apis.content.SESSION_API_PACKAGE_INSTALLED"; but it has no sense. Why are you using that string? It doesn't exists on any scripting referece. Please, stop copypasting code from another sites if you have not making it even work. – Windgate May 17 '21 at 11:13
  • @Windgate PACKAGE_INSTALLED_ACTION is just an intent action name. I thought this would be obvious for anyone reading the solution. I'm just following an official Android example: https://android.googlesource.com/platform/development/+/master/samples/ApiDemos/src/com/example/android/apis/content/InstallApkSessionApi.java I posted the same answer on the Xamarin forums. If this doesn't work for you, then you are clearly doing something wrong. No need to blame others for that. – brz May 17 '21 at 12:25
  • First of all, thanks for the clarification. I have spent days trying to make it work using that content in Xamarin forums, the content in the same link you posted and similar content on another sites. There are so many entries mixing const (PACKAGE_INSTALLED_ACTION) and hard-coded string (""com.example.android.apis.content.SESSION_API_PACKAGE_INSTALLED") for the action that I though that it was just a deprecated copy+paste with any kind of lost in translation issues. I'm going to clear all my project and follow that official Android example from the beggining, thanks. – Windgate May 17 '21 at 12:52
  • I would advice to test everything in an emulator first and not an actual device. Some phone manufacturers (Xiaomi and probably others too) like to include code into their android builds that break the sideloading of apps on default installations. I've spent multiple days until I figured that out, wondering why it wasn't working. – brz May 17 '21 at 13:01