diff --git a/.clang-format b/.clang-format index 0db8ab167..63f12b6c4 100644 --- a/.clang-format +++ b/.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 diff --git a/android/app/src/main/java/net/minetest/minetest/CopyZipTask.java b/android/app/src/main/java/net/minetest/minetest/CopyZipTask.java deleted file mode 100644 index 6d4b6ab0f..000000000 --- a/android/app/src/main/java/net/minetest/minetest/CopyZipTask.java +++ /dev/null @@ -1,82 +0,0 @@ -/* -Minetest -Copyright (C) 2014-2020 MoNTE48, Maksim Gamarnik -Copyright (C) 2014-2020 ubulem, Bektur Mambetov - -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 { - - private final WeakReference 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); - } - } -} diff --git a/android/app/src/main/java/net/minetest/minetest/GameActivity.java b/android/app/src/main/java/net/minetest/minetest/GameActivity.java index bdf764138..46fc9b1de 100644 --- a/android/app/src/main/java/net/minetest/minetest/GameActivity.java +++ b/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(); + } } diff --git a/android/app/src/main/java/net/minetest/minetest/MainActivity.java b/android/app/src/main/java/net/minetest/minetest/MainActivity.java index 2aa50d9ad..56615fca7 100644 --- a/android/app/src/main/java/net/minetest/minetest/MainActivity.java +++ b/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) { - if (mProgressBar != null) { - mProgressBar.setVisibility(View.VISIBLE); - mProgressBar.setProgress(progress); - } - mTextView.setVisibility(View.VISIBLE); - } else if (progress == FAILURE) { + 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) + } else if (progress == SUCCESS) { startNative(); + } else { + if (mProgressBar != null) { + mProgressBar.setVisibility(View.VISIBLE); + if (progress == INDETERMINATE) { + mProgressBar.setIndeterminate(true); + } else { + mProgressBar.setIndeterminate(false); + mProgressBar.setProgress(progress); + } + } + mTextView.setVisibility(View.VISIBLE); + 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() { diff --git a/android/app/src/main/java/net/minetest/minetest/UnzipService.java b/android/app/src/main/java/net/minetest/minetest/UnzipService.java index b69f7f36e..b513a7fe0 100644 --- a/android/app/src/main/java/net/minetest/minetest/UnzipService.java +++ b/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; + private static boolean isRunning = false; + public static synchronized boolean getIsRunning() { + return isRunning; + } + private static synchronized void setIsRunning(boolean v) { + isRunning = v; + } + public UnzipService() { super("net.minetest.minetest.UnzipService"); } - private void isDir(String dir, String location) { - File f = new File(location, dir); - if (!f.isDirectory()) - f.mkdirs(); - } - @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); } } diff --git a/android/app/src/main/java/net/minetest/minetest/Utils.java b/android/app/src/main/java/net/minetest/minetest/Utils.java new file mode 100644 index 000000000..b2553c844 --- /dev/null +++ b/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(); + } +} diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index e6f461f14..93508c3cb 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -1,4 +1,5 @@ + android:visibility="gone" + tools:visibility="visible" /> + android:visibility="gone" + tools:visibility="visible" /> diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 85238117f..99f948c99 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -3,9 +3,11 @@ Minetest Loading… + Migrating save data from old install… (this may take a while) Required permission wasn\'t granted, Minetest can\'t run without it Loading Minetest Less than 1 minute… Done + External storage isn\'t available. If you use an SDCard, please reinsert it. Otherwise, try restarting your phone or contacting the Minetest developers diff --git a/android/native/build.gradle b/android/native/build.gradle index 8ea6347b3..a7f095641 100644 --- a/android/native/build.gradle +++ b/android/native/build.gradle @@ -41,6 +41,10 @@ android { arguments 'NDEBUG=1' } } + + ndk { + debugSymbolLevel 'SYMBOL_TABLE' + } } } } diff --git a/src/porting_android.cpp b/src/porting_android.cpp index 29e95b8ca..c71fe5ad8 100644 --- a/src/porting_android.cpp +++ b/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"); + // 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); + } - 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 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,