4

I'm targeting my app to support 30 (R). I've notice that some apps are missing to choose when calling this:

baseActivity.startActivity(Intent(MediaStore.ACTION_IMAGE_CAPTURE))

When targeting to 29, this code shows several apps to choose before taking the picture:

  • Native camera app
  • B612 Camera app

After targeting to 30, the camera app is being opened directly (no option to choose).

I looked in the android 11 changes but didn't see anything special. Is there anything that needs to be change in my side?

Thanks for reading/helping

Udi Oshi
  • 6,787
  • 7
  • 47
  • 65

2 Answers2

6

Once your targetSdkVersion reaches 30, ACTION_IMAGE_CAPTURE will only display pre-installed camera apps, not user-installed apps.

mtotschnig
  • 1,238
  • 10
  • 30
CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • What are the alternative? If I want that my users will choose available third party apps to take pictures from without explicit the intent by setting specific package name? this sounds as a huge change for third party camera apps – Udi Oshi Aug 17 '20 at 11:18
  • @UdiOshi: It is going to suck, since even with `` in the manifest, you cannot query for all camera apps. An explicit `Intent` works, though, so if you know of a list of third-party camera apps that you want to support, you can craft a chooser for that. I outline the process in [this blog post](https://commonsware.com/blog/2020/08/16/action-image-capture-android-r.html). – CommonsWare Aug 17 '20 at 11:27
  • Well...after 9 years developing android, i'm tired of thinking what the heck Google are thinking...anyway, thanks very much! – Udi Oshi Aug 17 '20 at 11:43
  • 1
    I've found a (quite elaborate) workaround. Which is reading the AndroidManifest.xml's of the installed apps yourself to find out the IntentFilters. Bypassing the IntentResolver. See: https://github.com/frankkienl/Camera11 – FrankkieNL Aug 23 '20 at 14:44
  • @FrankkieNL: That is delightfully sneaky! I worry about it eventually triggering a ban by a bot. They have already indicated that using `` for non-justifiable scenarios may result in bans. Your technique, beyond this camera issue, basically lets you bypass ``-style limitations, by (in effect) replacing much of `PackageManager` with your own roster of components and filters from parsed manifests. – CommonsWare Aug 23 '20 at 15:03
  • @CommonsWare I didn't think of that, good point. Better not risk a ban. I'll make sure to add a warning to the answer and Github. – FrankkieNL Aug 23 '20 at 15:12
  • 1
    @CommonsWare two tiny updates on your [blog post](https://commonsware.com/blog/2020/08/16/action-image-capture-android-r.html): 1) starting with API 29 `EXTRA_INITIAL_INTENTS` can only accept at most 2 intents. 2) a potentially more user-friendly workaround is allow the user to choose from the list of installed apps one that's a third party camera, and then send an explicit intent from prefs to capture. This pushes the burden to the user, but probably still better than not having their "favorite camera app" on the hardcoded list. Except of course for `QUERY_ALL_PACKAGES` from API 30 ‍♂️. – TWiStErRob Aug 26 '23 at 17:36
  • @TWiStErRob: Thanks! At least when I wrote that post, `queryIntentActivities()` did not work for this -- I covered that in the post. Is it working for you? I did not try `QUERY_ALL_PACKAGES`, because that probably would not be something Google would not accept. And thanks on the `EXTRA_INITIAL_INTENTS` point! – CommonsWare Aug 26 '23 at 18:00
  • 1
    @CommonsWare `queryIntentActivities()` still only lists the system camera (Android 13), but `QUERY_ALL_PACKAGES` + user selection + explicit intent would work, only Google probably also thought about this and they made list all apps "insecure". So sadly I'm pointing out no new workaround, just more restrictions. – TWiStErRob Aug 26 '23 at 19:12
1

I've found a workaround;
TL;DR: Read the AndroidManifest.xml's of the apps yourself to find the camera apps.
Note: This may result in your app being banned from the store.

Step 1: Using the PackageManager, create a list of all apps that have the Camera-permission granted.

public static List<PackageInfo> getPackageInfosWithCameraPermission(Context context){
    //Get a list of compatible apps
    PackageManager pm = context.getPackageManager();
    List<PackageInfo> installedPackages = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS);
    ArrayList<PackageInfo> cameraPermissionPackages = new ArrayList<PackageInfo>();
    //filter out only camera apps
    for (PackageInfo somePackage : installedPackages) {
        //- A camera app should have the Camera permission
        boolean hasCameraPermission = false;
        if (somePackage.requestedPermissions == null || somePackage.requestedPermissions.length == 0) {
            continue;
        }
        for (String requestPermission : somePackage.requestedPermissions) {
            if (requestPermission.equals(Manifest.permission.CAMERA)) {
                //Ask for Camera permission, now see if it's granted.
                if (pm.checkPermission(Manifest.permission.CAMERA, somePackage.packageName) == PackageManager.PERMISSION_GRANTED) {
                    hasCameraPermission = true;
                    break;
                }
            }
        }
        if (hasCameraPermission) {
            cameraPermissionPackages.add(somePackage);
        }
    }
    return cameraPermissionPackages;
}

Step 2: Get the AndroidManifest from the APK-file (from PackageInfo)

public static Document readAndroidManifestFromPackageInfo(PackageInfo packageInfo) {
    File publicSourceDir = new File(packageInfo.applicationInfo.publicSourceDir);
    
    //Get AndroidManifest.xml from APK
    ZipFile apkZipFile = new ZipFile(apkFile, ZipFile.OPEN_READ);
    ZipEntry manifestEntry = apkZipFile.getEntry("AndroidManifest.xml");
    InputStream manifestInputStream = apkZipFile.getInputStream(manifestEntry);
    try {
        Document doc = new CompressedXmlParser().parseDOM(manifestInputStream);
        return doc;
    } catch (Exception e) {
        throw new IOException("Error reading AndroidManifest", e);
    }
}

Step 3: Read the AndroidManifest to find the Activities with the correct IntentFilter(s)

public static List<ComponentName> getCameraComponentNamesFromDocument(Document doc) {
    @SuppressLint("InlinedApi")
    String[] correctActions = {MediaStore.ACTION_IMAGE_CAPTURE, MediaStore.ACTION_IMAGE_CAPTURE_SECURE, MediaStore.ACTION_VIDEO_CAPTURE};
    ArrayList<ComponentName> componentNames = new ArrayList<ComponentName>();
    Element manifestElement = (Element) doc.getElementsByTagName("manifest").item(0);
    String packageName = manifestElement.getAttribute("package");
    Element applicationElement = (Element) manifestElement.getElementsByTagName("application").item(0);
    NodeList activities = applicationElement.getElementsByTagName("activity");
    for (int i = 0; i < activities.getLength(); i++) {
        Element activityElement = (Element) activities.item(i);
        String activityName = activityElement.getAttribute("android:name");
        NodeList intentFiltersList = activityElement.getElementsByTagName("intent-filter");
        for (int j = 0; j < intentFiltersList.getLength(); j++) {
            Element intentFilterElement = (Element) intentFiltersList.item(j);
            NodeList actionsList = intentFilterElement.getElementsByTagName("action");
            for (int k = 0; k < actionsList.getLength(); k++) {
                Element actionElement = (Element) actionsList.item(k);
                String actionName = actionElement.getAttribute("android:name");
                for (String correctAction : correctActions) {
                    if (actionName.equals(correctAction)) {
                        //this activity has an intent filter with a correct action, add this to the list.
                        componentNames.add(new ComponentName(packageName, activityName));
                    }
                }
            }
        }
    }
    return componentNames;
}

Step 4: Create a list of all Camera Apps

List<> cameraApps = new ArrayList<>();
for (PackageInfo somePackage : cameraPermissionPackages) {
            Document doc = readAndroidManifestFromPackageInfo(somePackage);
            List<ComponentName> componentNames = getCameraComponentNamesFromDocument(doc);
            if (componentNames.size() == 0) {
                continue; //This is not a Camera app
            }
            cameraApps.add(cameraApp);
    }

Step 5: Present list of Camera Apps to the user.
Just create a dialog or something.

I've worked it out into a library: https://github.com/frankkienl/Camera11

FrankkieNL
  • 711
  • 9
  • 22