Browse Source

Use scoped app storage on Android (#11466)

From November 2021, the Play Store will no longer be accepting
apps which use the deprecated getExternalStorageDirectory() API.

Therefore, this commit replaces uses of deprecated API with the new
scoped API (`getExternalFilesDir()` and `getExternalCacheDir()`).
It also provides a temporary migration to move user data from the
shared external directory to new storage.

Fixes #2097,  #11417 and #11118
rubenwardy 2 years ago
parent
commit
6901c5fae5

+ 5 - 4
.clang-format

@@ -1,6 +1,7 @@
 BasedOnStyle: LLVM
-IndentWidth: 8
+IndentWidth: 4
 UseTab: Always
+TabWidth: 4
 BreakBeforeBraces: Custom
 Standard: Cpp11
 BraceWrapping:
@@ -16,7 +17,7 @@ BraceWrapping:
 FixNamespaceComments: false
 AllowShortIfStatementsOnASingleLine: false
 IndentCaseLabels: false
-AccessModifierOffset: -8
+AccessModifierOffset: -4
 ColumnLimit: 90
 AllowShortFunctionsOnASingleLine: InlineOnly
 SortIncludes: false
@@ -26,7 +27,7 @@ IncludeCategories:
   - Regex:           '^<.*'
     Priority:        1
 AlignAfterOpenBracket: DontAlign
-ContinuationIndentWidth: 16
-ConstructorInitializerIndentWidth: 16
+ContinuationIndentWidth: 8
+ConstructorInitializerIndentWidth: 8
 BreakConstructorInitializers: AfterColon
 AlwaysBreakTemplateDeclarations: Yes

+ 0 - 82
android/app/src/main/java/net/minetest/minetest/CopyZipTask.java

@@ -1,82 +0,0 @@
-/*
-Minetest
-Copyright (C) 2014-2020 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua>
-Copyright (C) 2014-2020 ubulem,  Bektur Mambetov <berkut87@gmail.com>
-
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU Lesser General Public License as published by
-the Free Software Foundation; either version 2.1 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Lesser General Public License for more details.
-
-You should have received a copy of the GNU Lesser General Public License along
-with this program; if not, write to the Free Software Foundation, Inc.,
-51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-*/
-
-package net.minetest.minetest;
-
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.widget.Toast;
-
-import androidx.appcompat.app.AppCompatActivity;
-
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.lang.ref.WeakReference;
-
-public class CopyZipTask extends AsyncTask<String, Void, String> {
-
-	private final WeakReference<AppCompatActivity> activityRef;
-
-	CopyZipTask(AppCompatActivity activity) {
-		activityRef = new WeakReference<>(activity);
-	}
-
-	protected String doInBackground(String... params) {
-		copyAsset(params[0]);
-		return params[0];
-	}
-
-	@Override
-	protected void onPostExecute(String result) {
-		startUnzipService(result);
-	}
-
-	private void copyAsset(String zipName) {
-		String filename = zipName.substring(zipName.lastIndexOf("/") + 1);
-		try (InputStream in = activityRef.get().getAssets().open(filename);
-		     OutputStream out = new FileOutputStream(zipName)) {
-			copyFile(in, out);
-		} catch (IOException e) {
-			AppCompatActivity activity = activityRef.get();
-			if (activity != null) {
-				activity.runOnUiThread(() -> Toast.makeText(activityRef.get(), e.getLocalizedMessage(), Toast.LENGTH_LONG).show());
-			}
-			cancel(true);
-		}
-	}
-
-	private void copyFile(InputStream in, OutputStream out) throws IOException {
-		byte[] buffer = new byte[1024];
-		int read;
-		while ((read = in.read(buffer)) != -1)
-			out.write(buffer, 0, read);
-	}
-
-	private void startUnzipService(String file) {
-		Intent intent = new Intent(activityRef.get(), UnzipService.class);
-		intent.putExtra(UnzipService.EXTRA_KEY_IN_FILE, file);
-		AppCompatActivity activity = activityRef.get();
-		if (activity != null) {
-			activity.startService(intent);
-		}
-	}
-}

+ 8 - 0
android/app/src/main/java/net/minetest/minetest/GameActivity.java

@@ -171,4 +171,12 @@ public class GameActivity extends NativeActivity {
 		Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
 		startActivity(browserIntent);
 	}
+
+	public String getUserDataPath() {
+		return Utils.getUserDataDirectory(this).getAbsolutePath();
+	}
+
+	public String getCachePath() {
+		return Utils.getCacheDirectory(this).getAbsolutePath();
+	}
 }

+ 47 - 16
android/app/src/main/java/net/minetest/minetest/MainActivity.java

@@ -29,12 +29,14 @@ import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Environment;
 import android.view.View;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 import android.widget.Toast;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.core.app.ActivityCompat;
 import androidx.core.content.ContextCompat;
@@ -43,11 +45,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
-import static net.minetest.minetest.UnzipService.ACTION_FAILURE;
-import static net.minetest.minetest.UnzipService.ACTION_PROGRESS;
-import static net.minetest.minetest.UnzipService.ACTION_UPDATE;
-import static net.minetest.minetest.UnzipService.FAILURE;
-import static net.minetest.minetest.UnzipService.SUCCESS;
+import static net.minetest.minetest.UnzipService.*;
 
 public class MainActivity extends AppCompatActivity {
 	private final static int versionCode = BuildConfig.VERSION_CODE;
@@ -56,26 +54,40 @@ public class MainActivity extends AppCompatActivity {
 			new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
 	private static final String SETTINGS = "MinetestSettings";
 	private static final String TAG_VERSION_CODE = "versionCode";
+
 	private ProgressBar mProgressBar;
 	private TextView mTextView;
 	private SharedPreferences sharedPreferences;
+
 	private final BroadcastReceiver myReceiver = new BroadcastReceiver() {
 		@Override
 		public void onReceive(Context context, Intent intent) {
 			int progress = 0;
-			if (intent != null)
+			@StringRes int message = 0;
+			if (intent != null) {
 				progress = intent.getIntExtra(ACTION_PROGRESS, 0);
-			if (progress >= 0) {
+				message = intent.getIntExtra(ACTION_PROGRESS_MESSAGE, 0);
+			}
+
+			if (progress == FAILURE) {
+				Toast.makeText(MainActivity.this, intent.getStringExtra(ACTION_FAILURE), Toast.LENGTH_LONG).show();
+				finish();
+			} else if (progress == SUCCESS) {
+				startNative();
+			} else {
 				if (mProgressBar != null) {
 					mProgressBar.setVisibility(View.VISIBLE);
-					mProgressBar.setProgress(progress);
+					if (progress == INDETERMINATE) {
+						mProgressBar.setIndeterminate(true);
+					} else {
+						mProgressBar.setIndeterminate(false);
+						mProgressBar.setProgress(progress);
+					}
 				}
 				mTextView.setVisibility(View.VISIBLE);
-			} else if (progress == FAILURE) {
-				Toast.makeText(MainActivity.this, intent.getStringExtra(ACTION_FAILURE), Toast.LENGTH_LONG).show();
-				finish();
-			} else if (progress == SUCCESS)
-				startNative();
+				if (message != 0)
+					mTextView.setText(message);
+			}
 		}
 	};
 
@@ -88,6 +100,7 @@ public class MainActivity extends AppCompatActivity {
 		mProgressBar = findViewById(R.id.progressBar);
 		mTextView = findViewById(R.id.textView);
 		sharedPreferences = getSharedPreferences(SETTINGS, Context.MODE_PRIVATE);
+
 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
 			checkPermission();
 		else
@@ -120,6 +133,7 @@ public class MainActivity extends AppCompatActivity {
 				if (grantResult != PackageManager.PERMISSION_GRANTED) {
 					Toast.makeText(this, R.string.not_granted, Toast.LENGTH_LONG).show();
 					finish();
+					return;
 				}
 			}
 			checkAppVersion();
@@ -127,10 +141,27 @@ public class MainActivity extends AppCompatActivity {
 	}
 
 	private void checkAppVersion() {
-		if (sharedPreferences.getInt(TAG_VERSION_CODE, 0) == versionCode)
+		if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+			Toast.makeText(this, R.string.no_external_storage, Toast.LENGTH_LONG).show();
+			finish();
+			return;
+		}
+
+		if (UnzipService.getIsRunning()) {
+			mProgressBar.setVisibility(View.VISIBLE);
+			mProgressBar.setIndeterminate(true);
+			mTextView.setVisibility(View.VISIBLE);
+		} else if (sharedPreferences.getInt(TAG_VERSION_CODE, 0) == versionCode &&
+				Utils.isInstallValid(this)) {
 			startNative();
-		else
-			new CopyZipTask(this).execute(getCacheDir() + "/Minetest.zip");
+		} else {
+			mProgressBar.setVisibility(View.VISIBLE);
+			mProgressBar.setIndeterminate(true);
+			mTextView.setVisibility(View.VISIBLE);
+
+			Intent intent = new Intent(this, UnzipService.class);
+			startService(intent);
+		}
 	}
 
 	private void startNative() {

+ 139 - 42
android/app/src/main/java/net/minetest/minetest/UnzipService.java

@@ -24,16 +24,21 @@ import android.app.IntentService;
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Build;
 import android.os.Environment;
-import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
 
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
@@ -42,32 +47,61 @@ import java.util.zip.ZipInputStream;
 public class UnzipService extends IntentService {
 	public static final String ACTION_UPDATE = "net.minetest.minetest.UPDATE";
 	public static final String ACTION_PROGRESS = "net.minetest.minetest.PROGRESS";
+	public static final String ACTION_PROGRESS_MESSAGE = "net.minetest.minetest.PROGRESS_MESSAGE";
 	public static final String ACTION_FAILURE = "net.minetest.minetest.FAILURE";
-	public static final String EXTRA_KEY_IN_FILE = "file";
 	public static final int SUCCESS = -1;
 	public static final int FAILURE = -2;
+	public static final int INDETERMINATE = -3;
 	private final int id = 1;
 	private NotificationManager mNotifyManager;
 	private boolean isSuccess = true;
 	private String failureMessage;
 
-	public UnzipService() {
-		super("net.minetest.minetest.UnzipService");
+	private static boolean isRunning = false;
+	public static synchronized boolean getIsRunning() {
+		return isRunning;
+	}
+	private static synchronized void setIsRunning(boolean v) {
+		isRunning = v;
 	}
 
-	private void isDir(String dir, String location) {
-		File f = new File(location, dir);
-		if (!f.isDirectory())
-			f.mkdirs();
+	public UnzipService() {
+		super("net.minetest.minetest.UnzipService");
 	}
 
 	@Override
 	protected void onHandleIntent(Intent intent) {
-		createNotification();
-		unzip(intent);
+		Notification.Builder notificationBuilder = createNotification();
+		final File zipFile = new File(getCacheDir(), "Minetest.zip");
+		try {
+			setIsRunning(true);
+			File userDataDirectory = Utils.getUserDataDirectory(this);
+			if (userDataDirectory == null) {
+				throw new IOException("Unable to find user data directory");
+			}
+
+			try (InputStream in = this.getAssets().open(zipFile.getName())) {
+				try (OutputStream out = new FileOutputStream(zipFile)) {
+					int readLen;
+					byte[] readBuffer = new byte[16384];
+					while ((readLen = in.read(readBuffer)) != -1) {
+						out.write(readBuffer, 0, readLen);
+					}
+				}
+			}
+
+			migrate(notificationBuilder, userDataDirectory);
+			unzip(notificationBuilder, zipFile, userDataDirectory);
+		} catch (IOException e) {
+			isSuccess = false;
+			failureMessage = e.getLocalizedMessage();
+		} finally {
+			setIsRunning(false);
+			zipFile.delete();
+		}
 	}
 
-	private void createNotification() {
+	private Notification.Builder createNotification() {
 		String name = "net.minetest.minetest";
 		String channelId = "Minetest channel";
 		String description = "notifications from Minetest";
@@ -92,66 +126,129 @@ public class UnzipService extends IntentService {
 		} else {
 			builder = new Notification.Builder(this);
 		}
+
+		Intent notificationIntent = new Intent(this, MainActivity.class);
+		notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+			| Intent.FLAG_ACTIVITY_SINGLE_TOP);
+		PendingIntent intent = PendingIntent.getActivity(this, 0,
+			notificationIntent, 0);
+
 		builder.setContentTitle(getString(R.string.notification_title))
 				.setSmallIcon(R.mipmap.ic_launcher)
-				.setContentText(getString(R.string.notification_description));
+				.setContentText(getString(R.string.notification_description))
+				.setContentIntent(intent)
+				.setOngoing(true)
+				.setProgress(0, 0, true);
+
 		mNotifyManager.notify(id, builder.build());
+		return builder;
 	}
 
-	private void unzip(Intent intent) {
-		String zip = intent.getStringExtra(EXTRA_KEY_IN_FILE);
-		isDir("Minetest", Environment.getExternalStorageDirectory().toString());
-		String location = Environment.getExternalStorageDirectory() + File.separator + "Minetest" + File.separator;
+	private void unzip(Notification.Builder notificationBuilder, File zipFile, File userDataDirectory) throws IOException {
 		int per = 0;
-		int size = getSummarySize(zip);
-		File zipFile = new File(zip);
+
+		int size;
+		try (ZipFile zipSize = new ZipFile(zipFile)) {
+			size = zipSize.size();
+		}
+
 		int readLen;
-		byte[] readBuffer = new byte[8192];
+		byte[] readBuffer = new byte[16384];
 		try (FileInputStream fileInputStream = new FileInputStream(zipFile);
 		     ZipInputStream zipInputStream = new ZipInputStream(fileInputStream)) {
 			ZipEntry ze;
 			while ((ze = zipInputStream.getNextEntry()) != null) {
 				if (ze.isDirectory()) {
 					++per;
-					isDir(ze.getName(), location);
-				} else {
-					publishProgress(100 * ++per / size);
-					try (OutputStream outputStream = new FileOutputStream(location + ze.getName())) {
-						while ((readLen = zipInputStream.read(readBuffer)) != -1) {
-							outputStream.write(readBuffer, 0, readLen);
-						}
+					Utils.createDirs(userDataDirectory, ze.getName());
+					continue;
+				}
+				publishProgress(notificationBuilder, R.string.loading, 100 * ++per / size);
+				try (OutputStream outputStream = new FileOutputStream(
+						new File(userDataDirectory, ze.getName()))) {
+					while ((readLen = zipInputStream.read(readBuffer)) != -1) {
+						outputStream.write(readBuffer, 0, readLen);
 					}
 				}
-				zipFile.delete();
 			}
-		} catch (IOException e) {
-			isSuccess = false;
-			failureMessage = e.getLocalizedMessage();
 		}
 	}
 
-	private void publishProgress(int progress) {
+	void moveFileOrDir(@NonNull File src, @NonNull File dst) throws IOException {
+		try {
+			Process p = new ProcessBuilder("/system/bin/mv",
+				src.getAbsolutePath(), dst.getAbsolutePath()).start();
+			int exitcode = p.waitFor();
+			if (exitcode != 0)
+				throw new IOException("Move failed with exit code " + exitcode);
+		} catch (InterruptedException e) {
+			throw new IOException("Move operation interrupted");
+		}
+	}
+
+	boolean recursivelyDeleteDirectory(@NonNull File loc) {
+		try {
+			Process p = new ProcessBuilder("/system/bin/rm", "-rf",
+				loc.getAbsolutePath()).start();
+			return p.waitFor() == 0;
+		} catch (IOException | InterruptedException e) {
+			return false;
+		}
+	}
+
+	/**
+	 * Migrates user data from deprecated external storage to app scoped storage
+	 */
+	private void migrate(Notification.Builder notificationBuilder, File newLocation) throws IOException {
+		File oldLocation = new File(Environment.getExternalStorageDirectory(), "Minetest");
+		if (!oldLocation.isDirectory())
+			return;
+
+		publishProgress(notificationBuilder, R.string.migrating, 0);
+		newLocation.mkdir();
+
+		String[] dirs = new String[] { "worlds", "games", "mods", "textures", "client" };
+		for (int i = 0; i < dirs.length; i++) {
+			publishProgress(notificationBuilder, R.string.migrating, 100 * i / dirs.length);
+			File dir = new File(oldLocation, dirs[i]), dir2 = new File(newLocation, dirs[i]);
+			if (dir.isDirectory() && !dir2.isDirectory()) {
+				moveFileOrDir(dir, dir2);
+			}
+		}
+
+		for (String filename : new String[] { "minetest.conf" }) {
+			File file = new File(oldLocation, filename), file2 = new File(newLocation, filename);
+			if (file.isFile() && !file2.isFile()) {
+				moveFileOrDir(file, file2);
+			}
+		}
+
+		recursivelyDeleteDirectory(oldLocation);
+	}
+
+	private void publishProgress(@Nullable  Notification.Builder notificationBuilder, @StringRes int message, int progress) {
 		Intent intentUpdate = new Intent(ACTION_UPDATE);
 		intentUpdate.putExtra(ACTION_PROGRESS, progress);
-		if (!isSuccess) intentUpdate.putExtra(ACTION_FAILURE, failureMessage);
+		intentUpdate.putExtra(ACTION_PROGRESS_MESSAGE, message);
+		if (!isSuccess)
+			intentUpdate.putExtra(ACTION_FAILURE, failureMessage);
 		sendBroadcast(intentUpdate);
-	}
 
-	private int getSummarySize(String zip) {
-		int size = 0;
-		try {
-			ZipFile zipSize = new ZipFile(zip);
-			size += zipSize.size();
-		} catch (IOException e) {
-			Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
+		if (notificationBuilder != null) {
+			notificationBuilder.setContentText(getString(message));
+			if (progress == INDETERMINATE) {
+				notificationBuilder.setProgress(100, 50, true);
+			} else {
+				notificationBuilder.setProgress(100, progress, false);
+			}
+			mNotifyManager.notify(id, notificationBuilder.build());
 		}
-		return size;
 	}
 
 	@Override
 	public void onDestroy() {
 		super.onDestroy();
 		mNotifyManager.cancel(id);
-		publishProgress(isSuccess ? SUCCESS : FAILURE);
+		publishProgress(null, R.string.loading, isSuccess ? SUCCESS : FAILURE);
 	}
 }

+ 39 - 0
android/app/src/main/java/net/minetest/minetest/Utils.java

@@ -0,0 +1,39 @@
+package net.minetest.minetest;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import java.io.File;
+
+public class Utils {
+	public static @NonNull File createDirs(File root, String dir) {
+		File f = new File(root, dir);
+		if (!f.isDirectory())
+			f.mkdirs();
+
+		return f;
+	}
+
+	public static @Nullable File getUserDataDirectory(Context context) {
+		File extDir = context.getExternalFilesDir(null);
+		if (extDir == null) {
+			return null;
+		}
+
+		return createDirs(extDir, "Minetest");
+	}
+
+	public static @Nullable File getCacheDirectory(Context context) {
+		return context.getCacheDir();
+	}
+
+	public static boolean isInstallValid(Context context) {
+		File userDataDirectory = getUserDataDirectory(context);
+		return userDataDirectory != null && userDataDirectory.isDirectory() &&
+			new File(userDataDirectory, "games").isDirectory() &&
+			new File(userDataDirectory, "builtin").isDirectory() &&
+			new File(userDataDirectory, "client").isDirectory() &&
+			new File(userDataDirectory, "textures").isDirectory();
+	}
+}

+ 5 - 2
android/app/src/main/res/layout/activity_main.xml

@@ -1,4 +1,5 @@
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
 	android:id="@+id/activity_main"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent"
@@ -14,7 +15,8 @@
 		android:layout_marginRight="90dp"
 		android:indeterminate="false"
 		android:max="100"
-		android:visibility="gone" />
+		android:visibility="gone"
+		tools:visibility="visible" />
 
 	<TextView
 		android:id="@+id/textView"
@@ -25,6 +27,7 @@
 		android:background="@android:color/transparent"
 		android:text="@string/loading"
 		android:textColor="#FEFEFE"
-		android:visibility="gone" />
+		android:visibility="gone"
+		tools:visibility="visible" />
 
 </RelativeLayout>

+ 2 - 0
android/app/src/main/res/values/strings.xml

@@ -3,9 +3,11 @@
 
 	<string name="label">Minetest</string>
 	<string name="loading">Loading&#8230;</string>
+	<string name="migrating">Migrating save data from old install&#8230; (this may take a while)</string>
 	<string name="not_granted">Required permission wasn\'t granted, Minetest can\'t run without it</string>
 	<string name="notification_title">Loading Minetest</string>
 	<string name="notification_description">Less than 1 minute&#8230;</string>
 	<string name="ime_dialog_done">Done</string>
+	<string name="no_external_storage">External storage isn\'t available. If you use an SDCard, please reinsert it. Otherwise, try restarting your phone or contacting the Minetest developers</string>
 
 </resources>

+ 4 - 0
android/native/build.gradle

@@ -41,6 +41,10 @@ android {
 					arguments 'NDEBUG=1'
 				}
 			}
+
+			ndk {
+				debugSymbolLevel 'SYMBOL_TABLE'
+			}
 		}
 	}
 }

+ 27 - 40
src/porting_android.cpp

@@ -152,48 +152,35 @@ static std::string javaStringToUTF8(jstring js)
 	return str;
 }
 
-// Calls static method if obj is NULL
-static std::string getAndroidPath(
-		jclass cls, jobject obj, jmethodID mt_getAbsPath, const char *getter)
-{
-	// Get getter method
-	jmethodID mt_getter;
-	if (obj)
-		mt_getter = jnienv->GetMethodID(cls, getter, "()Ljava/io/File;");
-	else
-		mt_getter = jnienv->GetStaticMethodID(cls, getter, "()Ljava/io/File;");
-
-	// Call getter
-	jobject ob_file;
-	if (obj)
-		ob_file = jnienv->CallObjectMethod(obj, mt_getter);
-	else
-		ob_file = jnienv->CallStaticObjectMethod(cls, mt_getter);
-
-	// Call getAbsolutePath
-	auto js_path = (jstring) jnienv->CallObjectMethod(ob_file, mt_getAbsPath);
-
-	return javaStringToUTF8(js_path);
-}
-
 void initializePathsAndroid()
 {
-	// Get Environment class
-	jclass cls_Env = jnienv->FindClass("android/os/Environment");
-	// Get File class
-	jclass cls_File = jnienv->FindClass("java/io/File");
-	// Get getAbsolutePath method
-	jmethodID mt_getAbsPath = jnienv->GetMethodID(cls_File,
-				"getAbsolutePath", "()Ljava/lang/String;");
-	std::string path_storage = getAndroidPath(cls_Env, nullptr,
-				mt_getAbsPath, "getExternalStorageDirectory");
-
-	path_user    = path_storage + DIR_DELIM + PROJECT_NAME_C;
-	path_share   = path_storage + DIR_DELIM + PROJECT_NAME_C;
-	path_locale  = path_share + DIR_DELIM + "locale";
-	path_cache   = getAndroidPath(nativeActivity,
-			app_global->activity->clazz, mt_getAbsPath, "getCacheDir");
-	migrateCachePath();
+	// Set user and share paths
+	{
+		jmethodID getUserDataPath = jnienv->GetMethodID(nativeActivity,
+				"getUserDataPath", "()Ljava/lang/String;");
+		FATAL_ERROR_IF(getUserDataPath==nullptr,
+				"porting::initializePathsAndroid unable to find Java getUserDataPath method");
+		jobject result = jnienv->CallObjectMethod(app_global->activity->clazz, getUserDataPath);
+		const char *javachars = jnienv->GetStringUTFChars((jstring) result, nullptr);
+		path_user = javachars;
+		path_share = javachars;
+		path_locale  = path_share + DIR_DELIM + "locale";
+		jnienv->ReleaseStringUTFChars((jstring) result, javachars);
+	}
+
+	// Set cache path
+	{
+		jmethodID getCachePath = jnienv->GetMethodID(nativeActivity,
+				"getCachePath", "()Ljava/lang/String;");
+		FATAL_ERROR_IF(getCachePath==nullptr,
+				"porting::initializePathsAndroid unable to find Java getCachePath method");
+		jobject result = jnienv->CallObjectMethod(app_global->activity->clazz, getCachePath);
+		const char *javachars = jnienv->GetStringUTFChars((jstring) result, nullptr);
+		path_cache = javachars;
+		jnienv->ReleaseStringUTFChars((jstring) result, javachars);
+
+		migrateCachePath();
+	}
 }
 
 void showInputDialog(const std::string &acceptButton, const std::string &hint,