Search code examples
javaandroidcameraandroid-camera-intent

How to save photo Camera-App in Public directory


I would like to achieve the following:

  1. From MyApp, start the phone's Camera app via an intent
  2. Phone's Camera app saves photo in a public directory (DCIM or DCIM/Camera)
  3. Saved photo has a filename of my choosing.
  4. Saved photo is available in Gallery App
  5. My app edits the Exif after Camera App return focus to MyApp.

In my MainActivity, I use a boolean USE_ANDROID_EXTERNAL_STORAGE_PUBLIC_DIRECTORY to switch between:

  • Saving photo-file to ExternalFilesDir (in my case: /storage/emulated/0/Android/data/pub.openbook.labellor/files/Pictures)
  • Saving photo-file to Public Directory (in my case /storage/emulated/0/DCIM)

The Camera app will successfully save the photo-file in ExternalFilesDir, but will fail to save the file to its Public Directory. As this happens within the Camera App, I am unable to debug this.

My questions:

  • Is it even possible to have the Camera App save to Public Directory DCIM?
  • If so, how?
  • How do I make my the photo visible in the Gallery App?

(My function galleryAddPic() completes without crash, but does not achieve it purpose. The photo remains invisible in the Gallery App.)

I am working with:

  • AndroidStudio 4.0.1
  • SDK platform Android 10.0+(R), API 30
  • My test device has android.os.Build.VERSION.SDK_INT 23
package pub.openbook.labellor;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;

import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.FileProvider;

import android.os.Environment;
import android.provider.MediaStore;
import android.view.View;

import android.view.Menu;
import android.view.MenuItem;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.util.Log;
import android.widget.LinearLayout;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;

import static android.os.Environment.getExternalStoragePublicDirectory;


public class MainActivity extends AppCompatActivity {

    static final int REQUEST_IMAGE_CAPTURE = 1;
    static final int REQUEST_TAKE_PHOTO = 1;
    static final boolean USE_ANDROID_EXTERNAL_STORAGE_PUBLIC_DIRECTORY = true;

    private static final String IMAGES_FOLDER_NAME = "Camera";
    String stPathToJpgFile;

    View mainCoordinatorLayout;
    private static final String logTag = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // initialize layout:
        setContentView(R.layout.activity_main);
        // initialize variables:
        mainCoordinatorLayout = (View) findViewById(R.id.main_coordinator_layout);
        Toolbar toolbar = findViewById(R.id.toolbar);
        FloatingActionButton fab = findViewById(R.id.fab);
        // initialize layout & and components:
        setSupportActionBar(toolbar);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dispatchTakePictureIntent(view);
            }
        });


        LinearLayout checkboxContainer = (LinearLayout) findViewById(R.id.checkbox_container);
        CheckBox cb = new CheckBox(this);
        cb.setText("Tutlane");
        cb.setChecked(true);
        checkboxContainer.addView(cb);
        cb = new CheckBox(this);
        cb.setText("Another");
        cb.setChecked(false);
        checkboxContainer.addView(cb);
        cb = new CheckBox(this);
        cb.setText("Label threee");
        cb.setChecked(false);
        checkboxContainer.addView(cb);

        Log.d(logTag, "============ android.os.Build.VERSION.SDK_INT " + android.os.Build.VERSION.SDK_INT + "=====================");
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        Log.d(logTag, "onActivityResult() back from take picture intent;");

        if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
            Log.d(logTag, "onActivityResult() photo resides at"+stPathToJpgFile);
            galleryAddPic(stPathToJpgFile);
            ImageView imageView = (ImageView) findViewById(R.id.thumbnail_view);
            Context mContext;
            mContext = (Context)this;
            Bitmap d = new BitmapDrawable(mContext.getResources() , stPathToJpgFile).getBitmap();
            if (null != d) {

                int nh = (int) ( d.getHeight() * (512.0 / d.getWidth()) );
                Bitmap scaled = Bitmap.createScaledBitmap(d, 512, nh, true);
                imageView.setImageBitmap(scaled);
            }
        }
    }

    /*
    Use resident Camera App to take picture and save (filename has labels)
     */
    private void dispatchTakePictureIntent(View view) {

        // take picture with resident camera app:
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        // Ensure that there's a camera activity to handle the intent
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            // Create the File where the photo should go
            File photoFile = null;
            try {
                photoFile = jpgFile();
            } catch (IOException ex) {
                // Error occurred while creating the File
                Log.e(logTag, "jpgFile() throws IOException");
            }
            Log.d(logTag, "dispatchTakePictureIntent() photoFile = "+photoFile );
            // Continue only if the File was successfully created
            if (photoFile != null) {
                Uri photoURI = FileProvider.getUriForFile(this,
                        BuildConfig.APPLICATION_ID + ".provider",
                        photoFile);
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
                startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
            }
        }
    }

    /*

     */
    private File jpgFile() throws IOException {
        // File object to be returned:
        File jpgFile;

        // Create an image file name with datestamp and labels:
        String timeStamp = new SimpleDateFormat("yyMMdd").format(new Date());
        String imageFileName = timeStamp + ".label1";
        Log.d(logTag, "jpgFile() imageFileName = "+imageFileName );

        File externalFilesDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        Log.d(logTag, "jpgFile() externalFilesDir = "+externalFilesDir );

        // create the File object:
        if (USE_ANDROID_EXTERNAL_STORAGE_PUBLIC_DIRECTORY) {
            // Since getExternalStoragePublicDirectory() has been deprecated in Build.VERSION_CODES.Q and higher:
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                Log.d(logTag, "jpgFile() Build.VERSION_CODES.Q or higher");
                // get the Activity Context:
                Context mContext;
                mContext = (Context)this;
                ContentResolver resolver = mContext.getContentResolver();
                ContentValues contentValues = new ContentValues();
                contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, imageFileName);
                contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
                contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/" + IMAGES_FOLDER_NAME);
                Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
                jpgFile = new File(imageUri.getPath());
            } else {
                Log.d(logTag, "jpgFile() lower than Build.VERSION_CODES.Q");
                String stStorageDir = Environment.getExternalStoragePublicDirectory(
                        Environment.DIRECTORY_DCIM).toString() + File.separator + IMAGES_FOLDER_NAME;
                // make sure the directory string points to a directory that exists:
                if (!new File(stStorageDir).exists()) { new File(stStorageDir).mkdir(); }
                jpgFile = new File(stStorageDir, imageFileName + ".jpg");
            }
        } else {
            //write photos to directory private to this app:
            File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
            jpgFile = new File(storageDir, imageFileName + ".jpg");
        }
        // Save a file: path for use with ACTION_VIEW intents
        stPathToJpgFile = jpgFile.getAbsolutePath();
        return jpgFile;
    }
    /*
    Invoke the system's media scanner to add your photo to the Media Provider's database,
     making it available in the Android Gallery application and to other apps.
     */
    private void galleryAddPic (String stPathToPicFile) {
        Log.d(logTag, "galleryAddPic() stPathToPicFile "+stPathToPicFile);
        Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        File f = new File(stPathToPicFile);
        Uri contentUri = Uri.fromFile(f);
        mediaScanIntent.setData(contentUri);
        this.sendBroadcast(mediaScanIntent);
    }

    public Uri addImageToGallery(ContentResolver cr, String imgType, File filepath) {
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.TITLE, "player");
        values.put(MediaStore.Images.Media.DISPLAY_NAME, "player");
        values.put(MediaStore.Images.Media.DESCRIPTION, "");
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/" + imgType);
        values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis());
        values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
        values.put(MediaStore.Images.Media.DATA, filepath.toString());

        return cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
    }



}

Solution

  • Firstly, the reason Gallery apps do not see your photo is because it's saved here: /storage/emulated/0/Android/data/pub.openbook.labellor/files/Pictures

    The data/ folder is a private path for your app and MediaScanner will ignore it. You should only save photos there if they're not meant to be seen with the Gallery app. I recommend taking a look at the data and file storage overview to learn more.

    Is it even possible to have the Camera App save to Public Directory DCIM?

    It isn't necessarily "DCIM", but it is possible to save the image in the device's default pictures folder, yes. In fact, this is the usual folder you should use for a camera app.

    How do I make my the photo visible in the Gallery App?

    If the photo file is saved in the device's default pictures folder it should also be visible in any Gallery App after invoking the MediaScanner (your implementation is fine).

    If so, how?

    Rather than writing a custom example, I'd like to point you towards the official docs for taking photos. The section I linked should be exactly what you need, with a thorough explanation of all the required steps. Basically, you want to remove your current jpgFile() function and replace it with the suggested implementation from the docs.

    Note that, when following the recommended implementation, you don't need the if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) stuff since you're using MediaStore (which also works with earlier APIs).