44

I am working on a library to allow apps to self-update, for those that are being distributed outside of the Android Market.

My original plan was to include code that would download the APK file to internal storage, and then install it from there via a ContentProvider and a content:// Uri. However, when I tried that, the installer system dumped a "Skipping dir: " warning to LogCat and failed to actually install it. Once I switched to downloading the APK to external storage and using a file:// Uri with the ACTION_VIEW installer Intent, it worked.

The "Skipping dir:" message seems to be logged by parsePackage() in PackageParser, which seems to assume that it is working with a File. That would suggest that we cannot use content:// Uri values.

Has anyone successfully used ACTION_VIEW on a application/vnd.android.package-archive Intent with a content:// Uri? If so, was there some specific trick in setting up the ContentProvider that made it work?

Thanks!

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • and how excatly this ContentProvider looks (specially openFile implementation)? – Selvin Mar 09 '12 at 16:50
  • 1
    @Selvin: A `ContentProvider` with an `openFile()` implementation is what's needed to allow files to be served, and I used [an existing trivial implementation that I know works in general](https://github.com/commonsguy/cw-advandroid/tree/master/ContentProvider/Files) as the basis. My question is whether anyone has successfully gotten this to work, as my reading of the source code suggests that it is not supported, regardless of what the `ContentProvider` looks like. – CommonsWare Mar 09 '12 at 16:53
  • I haven't tried it, but have you seen the method described here: http://stackoverflow.com/a/4605040/377260 – Paul Burke Mar 12 '12 at 15:09
  • @iPaulPro: That would only work if you were building firmware. – CommonsWare Mar 12 '12 at 15:13

4 Answers4

27

The documentation for ACTION_INSTALL_PACKAGE is incorrect. It too will only accept files.

My only suggestion would therefore be to create a copy of the file in the applications file area, make it world readable, and clean up any left-over files at a later date.

Previous incorrect answer: In 4.0 and above there is a ACTION_INSTALL_PACKAGE which will accept a content:// URI (JavaDoc), but, prior to that, you're limited to installing via ACTION_VIEW which does assume the URI passed is a file:// URI.

Al Sutton
  • 3,904
  • 1
  • 22
  • 17
  • Have you tried this? I get the same results as with `ACTION_VIEW` when I use a `content://` scheme -- a "Skipping dir" LogCat message, and the installer bails. – CommonsWare Mar 13 '12 at 19:52
  • 2
    I had faith in the documentation, but it would appear that the documentation is wrong. The code servicing ACTION_INSTALL_PACKAGE will only accept files as well. Time to register a bug. Sorry. – Al Sutton Mar 14 '12 at 13:49
  • 1
    Well, at least I'm not completely crazy, then. Incompletely crazy, most certainly. :-) Thanks for your help! – CommonsWare Mar 14 '12 at 14:18
  • 3
    Note: the `content` scheme works on Android 7.0, though not on previous versions of Android. – CommonsWare Oct 20 '16 at 12:36
  • IMPORTANT - `Context.MODE_WORLD_READABLE` is deprecated and (from documentation:) "As of [Android] N attempting to use this mode will throw a SecurityException". I used `Context.MODE_WORLD_READABLE` to let the system access the dowloaded APK-file in the app's private storage-directory so I didn't need the permission for external storage but as of Android 7 this is no longer possible! – Micha F. Jul 22 '17 at 17:05
14

I would assume this is not possible, as the Java API doesn't seem to allow it. ContentProvider's openFile() returns a ParcelFileDescriptor, from which you can obtain a java.io.FileDescriptor. You can then use this FileDescriptor to open either a FileInputStream or a FileOutputStream. Unfortunately, you can't use it to open a RandomAccessFile (despite the fact that RandomAccessFile works on descriptors just the same as the others, the constructor you'd need is just missing from the API).

As APK files are ZIP files, which must be read out of order (you have to seek to the end to find the file directory), I assume the implementation of installation will require a RandomAccessFile, so it would not have been possible to support the case you're trying to implement.

lambda
  • 3,295
  • 1
  • 26
  • 32
Jules
  • 14,841
  • 9
  • 83
  • 130
  • 1
    I agree with your assessment, but while your analysis went down a different path than mine, it's still pretty much where I am -- "well, it doesn't *look* possible". I'm holding out for something a bit more definitive. :-) I'll accept your answer, though, if I don't get something better. Thanks! – CommonsWare Mar 10 '12 at 19:55
  • The PackageManagerService which handles installation creates a copy of the APK file and so does not require the original URI to provide a stream with seeking ability. – Al Sutton Mar 12 '12 at 18:36
  • I've read the inside of zips via a content provider (I download addons for one of my projects and they consist of a zip provided by a content provider). – HaMMeReD Mar 13 '12 at 23:25
  • Note that Android 7.0+ supports `content` `Uri` values now. – CommonsWare Jan 20 '17 at 12:22
6

I agree with Jules analysis, and I would add concrete precisions :

In PackageInstallerActivity, which is called by ACTION_VIEW on an apk, there is this in the onCreate() method:

315 String apkPath = mPackageURI.getPath();
316 File apkFile = new File(apkPath);

And before that, this method from PackageUtil is called:

73 public static  PackageParser.Package getPackageInfo(Uri packageURI) {
74     final String archiveFilePath = packageURI.getPath();
75     PackageParser packageParser = new PackageParser(archiveFilePath);
76     File sourceFile = new File(archiveFilePath);
77     DisplayMetrics metrics = new DisplayMetrics();
78     metrics.setToDefaults();
79     return packageParser.parsePackage(sourceFile, archiveFilePath, metrics, 0);
80 }

All this tends to confirm that the PackageManager do expects only File Uris.

The log you have, Skipping dir: is found in packageParser.parsePackage, which tests whether the path given in the Uri is a file or not.

njzk2
  • 38,969
  • 7
  • 69
  • 107
1

I have this in one of my applications which allows me access to local storage (user preference selectable before you have a go at me ;) )

import java.io.*;
import android.content.*;
import android.database.*;
import android.net.*;
import android.os.*;
import android.preference.PreferenceManager;
import android.util.Log;

public class LocalFileContentProvider extends ContentProvider {
   private static final String URI_PREFIX = "content://your.content.provider.as.per.manifest";

   public static String constructUri(String url) {
       Uri uri = Uri.parse(url);
       return uri.isAbsolute() ? url : URI_PREFIX + url;
   }

   @Override
   public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {

       SharedPreferences app_preferences = PreferenceManager.getDefaultSharedPreferences(getContext());

       boolean allowLocal = app_preferences.getBoolean("allowLocalFiles", false);

        if (allowLocal) {    
            try {
                File file = new File(uri.getPath());

                if (file.isDirectory())
                    return null;

                ParcelFileDescriptor parcel = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
                return parcel;
            } catch (Exception e) {
                return null;
            }
        } else {
            return null;
        }

   }

   @Override
   public boolean onCreate() {
       return true;
   }

   @Override
   public int delete(Uri uri, String s, String[] as) {
       throw new UnsupportedOperationException("Not supported by this provider");
   }

   @Override
   public String getType(Uri uri) {
       throw new UnsupportedOperationException("Not supported by this provider");
   }

   @Override
   public Uri insert(Uri uri, ContentValues contentvalues) {
       throw new UnsupportedOperationException("Not supported by this provider");
   }

   @Override
   public Cursor query(Uri uri, String[] as, String s, String[] as1, String s1) {
       throw new UnsupportedOperationException("Not supported by this provider");
   }

   @Override
   public int update(Uri uri, ContentValues contentvalues, String s, String[] as) {
       throw new UnsupportedOperationException("Not supported by this provider");
   }

}

My manifest contains

<provider android:name="it.automated.android.kiosk.se.LocalFileContentProvider"
      android:authorities="it.automated" />

and then to start the install (or action)

Intent viewIntent = new Intent(Intent.ACTION_VIEW);
viewIntent.setDataAndType(Uri.parse(url), mimeType);
CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
Fuzzy
  • 847
  • 9
  • 23
  • I will try that again. Your code is nearly identical to what I started with, which failed with the results as described in the question itself. Thanks! – CommonsWare Mar 12 '12 at 15:23
  • The issue is not on the provider side, so even if you used the API Demos code (http://developer.android.com/resources/samples/ApiDemos/src/com/example/android/apis/content/FileProvider.html) the issue would still persist. – Al Sutton Mar 12 '12 at 18:40
  • You can use a WebView as an intermediary which works perfectly well. – Fuzzy Mar 13 '12 at 18:39
  • Hi @CommonsWare I'm new here and beginner in android too, i just need your a little bit assistance regarding your question , can you brief me with a detailed reply how can i install another app with the help of an app, like by storing the .apk into an app etc – Haseeb Warriach Jan 26 '19 at 10:27
  • in short i want to bind an apk with another apk to install, is it possible? if possible with latest versions ,plz let me know how is it – Haseeb Warriach Jan 26 '19 at 10:29