Отредактированное растровое изображение сохраняет только нарисованные изменения без исходного изображения в общем хранилище и пустое растровое изображение во внутреннем хранилище

#java #android #canvas #bitmap #scoped-storage

Вопрос:

Я добавлял новую функцию в свой проект приложения для рисования, где пользователи могли открывать любую картинку из телефонной галереи и рисовать на ней. Но у меня возникли проблемы с сохранением нового растрового изображения. Вот подробная информация ниже :

В моем приложении есть 3 вида деятельности :

  • Основное действие в виде меню,
  • Действие RecyclerView, которое действует как галерея,
  • Рисование, в котором пользователи рисуют

До недавнего времени всякий раз, когда пользователь сохранял изображение, он создавал две его копии. Одно частное, которое будет храниться в личном хранилище приложения, недоступном для использования в галерее телефона, но отображается в активности собственной галереи приложения, и одно общедоступное изображение в средствах массовой информации, которое будет отображаться в галерее телефонов. Мой класс галереи проверяет внутреннюю папку приложения и перечисляет все файлы в ней для отображения, поэтому я также сохраняю их в личном каталоге приложения. Метод сохранения изображения состоит из двух разделов, которые решают проблемы с областью хранения с помощью api 28 и

Недавно я добавил функцию, в которой пользователи могут выбрать любую фотографию из галереи телефона и отредактировать ее (как изображения, созданные приложением, так и те, которые оно не создавало).Он использует функцию выбора намерения.

Ожидаемыми результатами являются :

  • Откройте картинку из галереи в упражнении рисования и нарисуйте поверх нее
  • Сохраните отредактированную версию в виде нового изображения как в частном порядке в хранилище приложения, так и в общедоступной общей копии в главной галерее телефонов

Текущие проблемы :

  • Изображение открывается и рисуется без проблем (как изображения, сделанные приложением, так и иностранные изображения)
  • Сохранение изображений приводит к тому, что приложение сохраняет пустое растровое изображение в частной галерее телефона и изменения (внесенные изменения) в общем хранилище на телефоне без фактического изображения, которое должно было быть отредактировано.

Вот код :

От MainActivty-код, который открывает изображения из галереи телефона для их редактирования.

     // checks permissions for reading external and scoped storage before opening image picker
    private void pickAnImage() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q amp;amp; checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            Log.d(TAG, "SDK >= Q , requesting permission");
            requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1000);
        } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q amp;amp; checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            Log.d(TAG, "SDK < Q , requesting permission");
            requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1000);
        } else {
            Log.d(TAG, "Permission exists, starting pick intent");
            Intent gallery = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
            startActivityForResult(gallery, IMAGE);
        }
    }

    // handles results from permissions and begins intent picker
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 1000) {
            Log.d(TAG, "result code good");
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                Log.d(TAG, "Permission is granted, starting pick");
                Intent gallery = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
                startActivityForResult(gallery, IMAGE);
            } else {
                Log.d(TAG, "While result code is good, permission was denied");
                Toast.makeText(MainActivity.this, "Permission Denied !", Toast.LENGTH_SHORT).show();
            }
        }
    }
    
    // handles results from intent picker and feeds the image path to the drawing activity for image editing
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (resultCode == RESULT_OK amp;amp; requestCode == IMAGE amp;amp; data != null) {
            Log.d(TAG, "Data from picked image is not null, creating uri and path");
            Uri selectedImageUri = data.getData();
            String picturePath = getPath(getApplicationContext(), selectedImageUri);
//            Log.d("Picture Path", picturePath);

            if (picturePath != null) {
                Log.d(TAG, "Path creating success, calling art activity");
                Intent intent = new Intent(getApplicationContext(), ArtActivity.class);
                intent.putExtra("image", picturePath);
                startActivity(intent);
            } else {
                Log.d(TAG, "Path was null");
                finish();
            }
        }else{
            Log.d(TAG, "Data seems to be null, aborting");
        }
    }

    // obtains the path from the picked image
    private static String getPath(Context context, Uri uri) {
        String result = null;
        String[] proj = {MediaStore.Images.Media.DATA};
        Cursor cursor = context.getContentResolver().query(uri, proj, null, null, null);
        if (cursor != null) {
            if (cursor.moveToFirst()) {
                int column_index = cursor.getColumnIndexOrThrow(proj[0]);
                result = cursor.getString(column_index);
                Toast.makeText(context, ""   result, Toast.LENGTH_SHORT).show();
            }
            cursor.close();
        }
        else {
            Toast.makeText(context.getApplicationContext(), "Failed to get image path , result is null or permission problem ?", Toast.LENGTH_SHORT).show();
            result = "Not found";
            Toast.makeText(context, ""   result, Toast.LENGTH_SHORT).show();
        }
        return resu<
    }
 

Below are methods that handle bitmap editing process :

 // Inside initialization method that sets up paint and bitmap objects before drawing
 if (bitmap != null) {
        // if bitmap object is not null it means that we are feeding existing bitmap to be edited, therefore we just scale it to screen size before giving it to canvas
        loadedBitmap = scale(bitmap, width, height);
    } else {
        // bitmap fed to this method is null therefore it means we are not editing existing picture so we create a new object to draw on
        this.bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888, true);
    }
 

Inside onDraw method that handles drawing as it happens :

 if (loadedBitmap != null) {
            // loadedBitmap not being null means we are editing existing image from above, we previously scaled it so now we feed it to the canvas for editing
            canvas.drawBitmap(loadedBitmap, 0, 0, paintLine);
            canvas.clipRect(0, 0, loadedBitmap.getWidth(), loadedBitmap.getHeight());
        } else {
            // we are not editing an image so we draw new empty bitmap to use
            canvas.drawBitmap(bitmap, 0, 0, tPaintline);
        }
 

And finally, here is the infamous save image method that is the root of current problems :

 @SuppressLint("WrongThread") // not sure what to do otherwise about this lint
public void saveImage() throws IOException {

    //create a filename and canvas
    String filename = "appName"   System.currentTimeMillis();
    Canvas canvas;

    // loadedBitmap is the edited one, bitmap is a new one if we did not edit anything
    if(loadedBitmap != null){
        canvas = new Canvas(loadedBitmap);
    } else{
        canvas = new Canvas(bitmap);
    }

    // save image handle for newer api that has scoped storage and updated code
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

            // feed all the data to content values
            OutputStream fos;
            ContentResolver resolver = context.getContentResolver();
            ContentValues contentValues = new ContentValues();
            contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, filename   ".jpg");
            contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg");
            contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
            
            
            Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
            fos = resolver.openOutputStream(Objects.requireNonNull(imageUri));
            draw(canvas);
            
            // compress correct bitmap, loaded is edited, bitmap is new art
            if(loadedBitmap != null){
                loadedBitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
            }else{
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
            }

            Objects.requireNonNull(fos).close();
            imageSaved = true;

    } else {

        // for api older then 28 before scoped storage
        // create app directory and a new image file
        ContextWrapper cw = new ContextWrapper(getContext());
        File directory = cw.getDir("files", Context.MODE_PRIVATE);
        pathString = cw.getDir("files", Context.MODE_PRIVATE).toString();
        File myPath = new File(directory, filename   ".jpg");
        FileOutputStream fileOutputStream = new FileOutputStream(myPath);
   
        // check permissions
        try {
            if(ContextCompat.checkSelfPermission(
                    context, Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
                    PackageManager.PERMISSION_GRANTED){

                draw(canvas);
                
                // save edited or new bitmap to private internal storage
                if(loadedBitmap != null){
                    MediaStore.Images.Media.insertImage(context.getContentResolver(), loadedBitmap, filename, "made with appName");
                }else{
                    MediaStore.Images.Media.insertImage(context.getContentResolver(), bitmap, filename, "made with appName");
                }
                
                // now also add a copy to shared storage so it will show up in phone's gallery
                addImageToGallery(myPath.getPath(), context);
                imageSaved = true;

            }else{
                // request permissions if not available
                requestPermissions((Activity) context,
                        new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE },
                        100);
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                fileOutputStream.flush();
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
 

Save image to gallery method used above :

     // for old api <28, it adds the image to shared storage and phone's gallery
    public static void addImageToGallery(final String filePath, final Context context) {

    ContentValues values = new ContentValues();

    values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
    values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
    values.put(MediaStore.MediaColumns.DATA, filePath);

    context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
}
 

Вот и все, если кто-нибудь знает растровое изображение, холст и разрешения (старые и новые), не стесняйтесь помочь. Я уверен, что изменения, которые мне нужны, невелики. Проверьте текущие проблемы выше, чтобы знать, что искать.