1

I want to create a database in my app. I used this code and it works properly on Android 9 and lower, but on Android 10+ I got a crash, and not only is the database not created but also no file directory is created in external storage.

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.miplan">

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    tools:ignore="ScopedStorage">
    
</uses-permission>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MiPlan">
        <activity
            android:name=".SplashActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".MainActivity"
            android:exported="false" />
        <activity
            android:name=".LogoActivity"
            android:exported="true">

        </activity>
    </application>

</manifest>

And this is MyDatabaseHelper:

package com.example.miplan;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper {

  private static final String DB_NAME = "myDatabase.SqLite";
  private static final int DB_VERSION = 1;

  public MyDatabaseHelper(@Nullable Context context) {
    super(context, SplashActivity.DB_DIR + "/" + DB_NAME, null, DB_VERSION);
  }

  @Override
  public void onCreate(SQLiteDatabase db) {
    String query = " CREATE TABLE 'person' (" +
      "'personId' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL , " +
      "'firstName' TEXT, " +
      "'lastName' TEXT, " +
      "'gender' INTEGER, " +
      "'email' TEXT UNIQUE, " +
      "'phoneNumber' TEXT UNIQUE)";
    // db.execSQL(query);
  }

  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
  }


}

SplashActivity:

package com.example.miplan;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;

import java.io.File;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

@SuppressLint("CustomSplashScreen")
public class SplashActivity extends AppCompatActivity {
  public static String SDCARD = Environment.getExternalStorageDirectory().getAbsolutePath();
  public static String BRAND = SDCARD + "/Self-learn";
  public static String APP_DIR = BRAND + "/MiPlan";
  public static String DB_DIR = APP_DIR + "/db";
  public static SQLiteDatabase database;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_begin);
    int grantResult = ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);

    if (grantResult == PackageManager.PERMISSION_GRANTED) {
      createAppDir();
    } else {
      askUserForPermission();
    }
  }

  private void askUserForPermission() {
    ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
  }

  @Override
  public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    switch (requestCode) {
      case 100:
        if (grantResults[0] == PackageManager.PERMISSION_DENIED) {

          AlertDialog.Builder dialog = new AlertDialog.Builder(this);
          dialog.setTitle("Error").setMessage("Write on External Storage is needed for this app").setPositiveButton("ask again", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
              askUserForPermission();
            }
          }).setCancelable(false).create().show();

        } else {
          createAppDir();
        }
        break;
    }
  }

  private void createAppDir() {
    File file = new File(DB_DIR);
    if (!file.exists()) {
      file.mkdirs();
      createDatabase();
    }
    createDatabase();

  }

  public void createDatabase() {
    MyDatabaseHelper dbHelper = new MyDatabaseHelper(SplashActivity.this);
    database = dbHelper.getWritableDatabase();
    Log.i("testtt", "this line is working well...");
    openWelcomeActivity();
  }

  private void openWelcomeActivity() {

    Intent intent = new Intent(this, LogoActivity.class);
    startActivity(intent);
    this.finish();
  }

And this is the Logcat error:

Caused by: android.database.sqlite.SQLiteCantOpenDatabaseException: unknown error (code 14 SQLITE_CANTOPEN): Could not open database at android.database.sqlite.SQLiteConnection.nativeOpen(Native Method)

Is there any changes in creating a database or writing in external storage in Android 10+?

bighugedev
  • 379
  • 1
  • 3
  • 11
  • You shouldn't request `WRITE_EXTERNAL_STORAGE` for Android 10+; it has no effect. Give that a try – Zain Mar 11 '23 at 13:36
  • 1
    You cannot write to arbitrary locations on external storage on Android 11+. Also, having live SQLite database in user-accessible storage is very risky -- if they try manipulating those files at the same time that your app does, you might run into data corruption. I recommend keeping your database in the default location and offering backup/restore or import/export features in your app. – CommonsWare Mar 11 '23 at 13:59
  • if (!file.exists()) if(! file.mkdirs()) return; – blackapps Mar 11 '23 at 14:03

1 Answers1

1

You shouldn't request WRITE_EXTERNAL_STORAGE for Android 10+; it has no effect.

In documentation:

Note: If your app targets Build.VERSION_CODES.R or higher, this permission has no effect.

So, you need to go ahead without requesting the permission for Android 10+

private void askUserForPermission() {
    if (SDK_INT < Build.VERSION_CODES.Q)
        ActivityCompat.requestPermissions(this, new String[]
                                    {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
    else
        createAppDir();
}

Notice that you are requesting permission with a deprecated API, check this question for the alternative API

Also, consider the worthy comment pointed out by CommonsWare

Update:

Caused by: android.database.sqlite.SQLiteCantOpenDatabaseException: unknown error
(code 14 SQLITE_CANTOPEN): Could not open database at
android.database.sqlite.SQLiteConnection.nativeOpen(Native Method)

As CommonsWare pointed out in this comment, developers are not allowed to write on arbitrary locations on external storage on Android 11+. And this exception is raised because of that.

To solve this, it requires the developer either:

  1. To allow the user to select a particular folder by navigating through the external/SD card using ACTION_CREATE_DOCUMENT intent Check here for further detail.

  2. Or to use the space allocated to the app.

If you want to consider the second option, instead of the shared Environment.getExternalStorageDirectory().getAbsolutePath() directory, you can write on the SD card getExternalFilesDir() or on the internal device storage with getFilesDir(). Both are allocated to your app, and no other app can access them normally.

So, you have to change the following code:

public static String SDCARD = Environment
                        .getExternalStorageDirectory().getAbsolutePath(); 

To something like:

public static String SDCARD = context.getFilesDir().getAbsolutePath(); 

// or

public static String SDCARD = context.getExternalFilesDir(null).getAbsolutePath(); 

I kept SDCARD as-is; but this doesn't indicate that it's the SD shared storage.

Zain
  • 37,492
  • 7
  • 60
  • 84
  • 1
    It has effect and is needed on all Android devices with Android version below 13. So yes it should be requested then. Throw away that doc. – blackapps Mar 11 '23 at 14:04
  • @blackapps Do you have any reference for that? I have an app up and running without requesting that permission API 10+; the callback of the permission dialog didn't even get triggered – Zain Mar 11 '23 at 14:15
  • Make sure to handle the `context` association to the `SDCARD` – Zain Mar 12 '23 at 17:20