aboutsummaryrefslogtreecommitdiff
path: root/android/app
diff options
context:
space:
mode:
authorNeroBurner <pyro4hell@gmail.com>2021-06-21 21:51:42 +0200
committerGitHub <noreply@github.com>2021-06-21 20:51:42 +0100
commita7143c2a8c48b234d78ec666193b942ae0b62ca3 (patch)
treec6ba5317ba853536d25709f80aadc4036c14dfd3 /android/app
parent7fdbf3f231976257a1c246c6ebcdbc5bb8fa5571 (diff)
downloadminetest-a7143c2a8c48b234d78ec666193b942ae0b62ca3.tar.gz
minetest-a7143c2a8c48b234d78ec666193b942ae0b62ca3.tar.bz2
minetest-a7143c2a8c48b234d78ec666193b942ae0b62ca3.zip
Move build/android directory to root of project (#11283)
Diffstat (limited to 'android/app')
-rw-r--r--android/app/build.gradle113
-rw-r--r--android/app/src/main/AndroidManifest.xml62
-rw-r--r--android/app/src/main/java/net/minetest/minetest/CopyZipTask.java82
-rw-r--r--android/app/src/main/java/net/minetest/minetest/CustomEditText.java45
-rw-r--r--android/app/src/main/java/net/minetest/minetest/GameActivity.java174
-rw-r--r--android/app/src/main/java/net/minetest/minetest/MainActivity.java153
-rw-r--r--android/app/src/main/java/net/minetest/minetest/UnzipService.java157
-rw-r--r--android/app/src/main/res/drawable/background.pngbin0 -> 83 bytes
-rw-r--r--android/app/src/main/res/drawable/bg.xml4
-rw-r--r--android/app/src/main/res/layout/activity_main.xml30
-rw-r--r--android/app/src/main/res/mipmap/ic_launcher.pngbin0 -> 5780 bytes
-rw-r--r--android/app/src/main/res/values/strings.xml11
-rw-r--r--android/app/src/main/res/values/styles.xml15
13 files changed, 846 insertions, 0 deletions
diff --git a/android/app/build.gradle b/android/app/build.gradle
new file mode 100644
index 000000000..b7d93ef0f
--- /dev/null
+++ b/android/app/build.gradle
@@ -0,0 +1,113 @@
+apply plugin: 'com.android.application'
+android {
+ compileSdkVersion 29
+ buildToolsVersion '30.0.3'
+ ndkVersion '22.0.7026061'
+ defaultConfig {
+ applicationId 'net.minetest.minetest'
+ minSdkVersion 16
+ targetSdkVersion 29
+ versionName "${versionMajor}.${versionMinor}.${versionPatch}"
+ versionCode project.versionCode
+ }
+
+ // load properties
+ Properties props = new Properties()
+ def propfile = file('../local.properties')
+ if (propfile.exists())
+ props.load(new FileInputStream(propfile))
+
+ if (props.getProperty('keystore') != null) {
+ signingConfigs {
+ release {
+ storeFile file(props['keystore'])
+ storePassword props['keystore.password']
+ keyAlias props['key']
+ keyPassword props['key.password']
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled true
+ signingConfig signingConfigs.release
+ }
+ }
+ }
+
+ // for multiple APKs
+ splits {
+ abi {
+ enable true
+ reset()
+ include 'armeabi-v7a', 'arm64-v8a'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+task prepareAssets() {
+ def assetsFolder = "build/assets"
+ def projRoot = "../.."
+ def gameToCopy = "minetest_game"
+
+ copy {
+ from "${projRoot}/minetest.conf.example", "${projRoot}/README.md" into assetsFolder
+ }
+ copy {
+ from "${projRoot}/doc/lgpl-2.1.txt" into "${assetsFolder}"
+ }
+ copy {
+ from "${projRoot}/builtin" into "${assetsFolder}/builtin"
+ }
+ copy {
+ from "${projRoot}/client/shaders" into "${assetsFolder}/client/shaders"
+ }
+ copy {
+ from "../native/deps/Android/Irrlicht/shaders" into "${assetsFolder}/client/shaders/Irrlicht"
+ }
+ copy {
+ from "${projRoot}/fonts" include "*.ttf" into "${assetsFolder}/fonts"
+ }
+ copy {
+ from "${projRoot}/games/${gameToCopy}" into "${assetsFolder}/games/${gameToCopy}"
+ }
+ /*copy {
+ // ToDo: fix broken locales
+ from "${projRoot}/po" into "${assetsFolder}/po"
+ }*/
+ copy {
+ from "${projRoot}/textures" into "${assetsFolder}/textures"
+ }
+
+ file("${assetsFolder}/.nomedia").text = "";
+
+ task zipAssets(type: Zip) {
+ archiveName "Minetest.zip"
+ from "${assetsFolder}"
+ destinationDir file("src/main/assets")
+ }
+}
+
+preBuild.dependsOn zipAssets
+
+// Map for the version code that gives each ABI a value.
+import com.android.build.OutputFile
+
+def abiCodes = ['armeabi-v7a': 0, 'arm64-v8a': 1]
+android.applicationVariants.all { variant ->
+ variant.outputs.each {
+ output ->
+ def abiName = output.getFilter(OutputFile.ABI)
+ output.versionCodeOverride = abiCodes.get(abiName, 0) + variant.versionCode
+ }
+}
+
+dependencies {
+ implementation project(':native')
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..fa93e7069
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,62 @@
+<?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="net.minetest.minetest"
+ android:installLocation="auto">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+ <!--
+ `android:requestLegacyExternalStorage="true"` is workaround for using `/sdcard`
+ instead of the `getFilesDir()` patch for assets. Check link below for more information:
+ https://developer.android.com/training/data-storage/compatibility
+ -->
+
+ <application
+ android:allowBackup="false"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/label"
+ android:requestLegacyExternalStorage="true"
+ android:resizeableActivity="false"
+ tools:ignore="UnusedAttribute">
+
+ <meta-data
+ android:name="android.max_aspect"
+ android:value="3.0" />
+
+ <activity
+ android:name=".MainActivity"
+ android:configChanges="orientation|keyboardHidden|navigation|screenSize"
+ android:maxAspectRatio="3.0"
+ android:screenOrientation="sensorLandscape"
+ android:theme="@style/AppTheme">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".GameActivity"
+ android:configChanges="orientation|keyboard|keyboardHidden|navigation|screenSize|smallestScreenSize"
+ android:hardwareAccelerated="true"
+ android:launchMode="singleTask"
+ android:maxAspectRatio="3.0"
+ android:screenOrientation="sensorLandscape"
+ android:theme="@style/AppTheme">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ <meta-data
+ android:name="android.app.lib_name"
+ android:value="Minetest" />
+ </activity>
+
+ <service
+ android:name=".UnzipService"
+ android:enabled="true"
+ android:exported="false" />
+ </application>
+
+</manifest>
diff --git a/android/app/src/main/java/net/minetest/minetest/CopyZipTask.java b/android/app/src/main/java/net/minetest/minetest/CopyZipTask.java
new file mode 100644
index 000000000..6d4b6ab0f
--- /dev/null
+++ b/android/app/src/main/java/net/minetest/minetest/CopyZipTask.java
@@ -0,0 +1,82 @@
+/*
+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);
+ }
+ }
+}
diff --git a/android/app/src/main/java/net/minetest/minetest/CustomEditText.java b/android/app/src/main/java/net/minetest/minetest/CustomEditText.java
new file mode 100644
index 000000000..8d0a503d0
--- /dev/null
+++ b/android/app/src/main/java/net/minetest/minetest/CustomEditText.java
@@ -0,0 +1,45 @@
+/*
+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.Context;
+import android.view.KeyEvent;
+import android.view.inputmethod.InputMethodManager;
+
+import androidx.appcompat.widget.AppCompatEditText;
+
+import java.util.Objects;
+
+public class CustomEditText extends AppCompatEditText {
+ public CustomEditText(Context context) {
+ super(context);
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ InputMethodManager mgr = (InputMethodManager)
+ getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ Objects.requireNonNull(mgr).hideSoftInputFromWindow(this.getWindowToken(), 0);
+ }
+ return false;
+ }
+}
diff --git a/android/app/src/main/java/net/minetest/minetest/GameActivity.java b/android/app/src/main/java/net/minetest/minetest/GameActivity.java
new file mode 100644
index 000000000..bdf764138
--- /dev/null
+++ b/android/app/src/main/java/net/minetest/minetest/GameActivity.java
@@ -0,0 +1,174 @@
+/*
+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.app.NativeActivity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.InputType;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+
+import androidx.appcompat.app.AlertDialog;
+
+import java.util.Objects;
+
+public class GameActivity extends NativeActivity {
+ static {
+ System.loadLibrary("c++_shared");
+ System.loadLibrary("Minetest");
+ }
+
+ private int messageReturnCode = -1;
+ private String messageReturnValue = "";
+
+ public static native void putMessageBoxResult(String text);
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ private void makeFullScreen() {
+ if (Build.VERSION.SDK_INT >= 19)
+ this.getWindow().getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (hasFocus)
+ makeFullScreen();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ makeFullScreen();
+ }
+
+ @Override
+ public void onBackPressed() {
+ // Ignore the back press so Minetest can handle it
+ }
+
+ public void showDialog(String acceptButton, String hint, String current, int editType) {
+ runOnUiThread(() -> showDialogUI(hint, current, editType));
+ }
+
+ private void showDialogUI(String hint, String current, int editType) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ LinearLayout container = new LinearLayout(this);
+ container.setOrientation(LinearLayout.VERTICAL);
+ builder.setView(container);
+ AlertDialog alertDialog = builder.create();
+ EditText editText;
+ // For multi-line, do not close the dialog after pressing back button
+ if (editType == 1) {
+ editText = new EditText(this);
+ } else {
+ editText = new CustomEditText(this);
+ }
+ container.addView(editText);
+ editText.setMaxLines(8);
+ editText.requestFocus();
+ editText.setHint(hint);
+ editText.setText(current);
+ final InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+ Objects.requireNonNull(imm).toggleSoftInput(InputMethodManager.SHOW_FORCED,
+ InputMethodManager.HIDE_IMPLICIT_ONLY);
+ if (editType == 1)
+ editText.setInputType(InputType.TYPE_CLASS_TEXT |
+ InputType.TYPE_TEXT_FLAG_MULTI_LINE);
+ else if (editType == 3)
+ editText.setInputType(InputType.TYPE_CLASS_TEXT |
+ InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ else
+ editText.setInputType(InputType.TYPE_CLASS_TEXT);
+ editText.setSelection(editText.getText().length());
+ editText.setOnKeyListener((view, keyCode, event) -> {
+ // For multi-line, do not submit the text after pressing Enter key
+ if (keyCode == KeyEvent.KEYCODE_ENTER && editType != 1) {
+ imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
+ messageReturnCode = 0;
+ messageReturnValue = editText.getText().toString();
+ alertDialog.dismiss();
+ return true;
+ }
+ return false;
+ });
+ // For multi-line, add Done button since Enter key does not submit text
+ if (editType == 1) {
+ Button doneButton = new Button(this);
+ container.addView(doneButton);
+ doneButton.setText(R.string.ime_dialog_done);
+ doneButton.setOnClickListener((view -> {
+ imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
+ messageReturnCode = 0;
+ messageReturnValue = editText.getText().toString();
+ alertDialog.dismiss();
+ }));
+ }
+ alertDialog.show();
+ alertDialog.setOnCancelListener(dialog -> {
+ getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
+ messageReturnValue = current;
+ messageReturnCode = -1;
+ });
+ }
+
+ public int getDialogState() {
+ return messageReturnCode;
+ }
+
+ public String getDialogValue() {
+ messageReturnCode = -1;
+ return messageReturnValue;
+ }
+
+ public float getDensity() {
+ return getResources().getDisplayMetrics().density;
+ }
+
+ public int getDisplayHeight() {
+ return getResources().getDisplayMetrics().heightPixels;
+ }
+
+ public int getDisplayWidth() {
+ return getResources().getDisplayMetrics().widthPixels;
+ }
+
+ public void openURI(String uri) {
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
+ startActivity(browserIntent);
+ }
+}
diff --git a/android/app/src/main/java/net/minetest/minetest/MainActivity.java b/android/app/src/main/java/net/minetest/minetest/MainActivity.java
new file mode 100644
index 000000000..2aa50d9ad
--- /dev/null
+++ b/android/app/src/main/java/net/minetest/minetest/MainActivity.java
@@ -0,0 +1,153 @@
+/*
+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.Manifest;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+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;
+
+public class MainActivity extends AppCompatActivity {
+ private final static int versionCode = BuildConfig.VERSION_CODE;
+ private final static int PERMISSIONS = 1;
+ private static final String[] REQUIRED_SDK_PERMISSIONS =
+ 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)
+ 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) {
+ Toast.makeText(MainActivity.this, intent.getStringExtra(ACTION_FAILURE), Toast.LENGTH_LONG).show();
+ finish();
+ } else if (progress == SUCCESS)
+ startNative();
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ IntentFilter filter = new IntentFilter(ACTION_UPDATE);
+ registerReceiver(myReceiver, filter);
+ 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
+ checkAppVersion();
+ }
+
+ private void checkPermission() {
+ final List<String> missingPermissions = new ArrayList<>();
+ for (final String permission : REQUIRED_SDK_PERMISSIONS) {
+ final int result = ContextCompat.checkSelfPermission(this, permission);
+ if (result != PackageManager.PERMISSION_GRANTED)
+ missingPermissions.add(permission);
+ }
+ if (!missingPermissions.isEmpty()) {
+ final String[] permissions = missingPermissions
+ .toArray(new String[0]);
+ ActivityCompat.requestPermissions(this, permissions, PERMISSIONS);
+ } else {
+ final int[] grantResults = new int[REQUIRED_SDK_PERMISSIONS.length];
+ Arrays.fill(grantResults, PackageManager.PERMISSION_GRANTED);
+ onRequestPermissionsResult(PERMISSIONS, REQUIRED_SDK_PERMISSIONS, grantResults);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (requestCode == PERMISSIONS) {
+ for (int grantResult : grantResults) {
+ if (grantResult != PackageManager.PERMISSION_GRANTED) {
+ Toast.makeText(this, R.string.not_granted, Toast.LENGTH_LONG).show();
+ finish();
+ }
+ }
+ checkAppVersion();
+ }
+ }
+
+ private void checkAppVersion() {
+ if (sharedPreferences.getInt(TAG_VERSION_CODE, 0) == versionCode)
+ startNative();
+ else
+ new CopyZipTask(this).execute(getCacheDir() + "/Minetest.zip");
+ }
+
+ private void startNative() {
+ sharedPreferences.edit().putInt(TAG_VERSION_CODE, versionCode).apply();
+ Intent intent = new Intent(this, GameActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ }
+
+ @Override
+ public void onBackPressed() {
+ // Prevent abrupt interruption when copy game files from assets
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ unregisterReceiver(myReceiver);
+ }
+}
diff --git a/android/app/src/main/java/net/minetest/minetest/UnzipService.java b/android/app/src/main/java/net/minetest/minetest/UnzipService.java
new file mode 100644
index 000000000..b69f7f36e
--- /dev/null
+++ b/android/app/src/main/java/net/minetest/minetest/UnzipService.java
@@ -0,0 +1,157 @@
+/*
+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.app.IntentService;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Environment;
+import android.widget.Toast;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+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_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;
+ private final int id = 1;
+ private NotificationManager mNotifyManager;
+ private boolean isSuccess = true;
+ private String failureMessage;
+
+ 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);
+ }
+
+ private void createNotification() {
+ String name = "net.minetest.minetest";
+ String channelId = "Minetest channel";
+ String description = "notifications from Minetest";
+ Notification.Builder builder;
+ if (mNotifyManager == null)
+ mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ int importance = NotificationManager.IMPORTANCE_LOW;
+ NotificationChannel mChannel = null;
+ if (mNotifyManager != null)
+ mChannel = mNotifyManager.getNotificationChannel(channelId);
+ if (mChannel == null) {
+ mChannel = new NotificationChannel(channelId, name, importance);
+ mChannel.setDescription(description);
+ // Configure the notification channel, NO SOUND
+ mChannel.setSound(null, null);
+ mChannel.enableLights(false);
+ mChannel.enableVibration(false);
+ mNotifyManager.createNotificationChannel(mChannel);
+ }
+ builder = new Notification.Builder(this, channelId);
+ } else {
+ builder = new Notification.Builder(this);
+ }
+ builder.setContentTitle(getString(R.string.notification_title))
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setContentText(getString(R.string.notification_description));
+ mNotifyManager.notify(id, builder.build());
+ }
+
+ 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;
+ int per = 0;
+ int size = getSummarySize(zip);
+ File zipFile = new File(zip);
+ int readLen;
+ byte[] readBuffer = new byte[8192];
+ 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);
+ }
+ }
+ }
+ zipFile.delete();
+ }
+ } catch (IOException e) {
+ isSuccess = false;
+ failureMessage = e.getLocalizedMessage();
+ }
+ }
+
+ private void publishProgress(int progress) {
+ Intent intentUpdate = new Intent(ACTION_UPDATE);
+ intentUpdate.putExtra(ACTION_PROGRESS, progress);
+ 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();
+ }
+ return size;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mNotifyManager.cancel(id);
+ publishProgress(isSuccess ? SUCCESS : FAILURE);
+ }
+}
diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png
new file mode 100644
index 000000000..43bd6089e
--- /dev/null
+++ b/android/app/src/main/res/drawable/background.png
Binary files differ
diff --git a/android/app/src/main/res/drawable/bg.xml b/android/app/src/main/res/drawable/bg.xml
new file mode 100644
index 000000000..903335ed9
--- /dev/null
+++ b/android/app/src/main/res/drawable/bg.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/background"
+ android:tileMode="repeat" />
diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..e6f461f14
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,30 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/activity_main"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/bg">
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="@style/CustomProgressBar"
+ android:layout_width="match_parent"
+ android:layout_height="30dp"
+ android:layout_centerInParent="true"
+ android:layout_marginLeft="90dp"
+ android:layout_marginRight="90dp"
+ android:indeterminate="false"
+ android:max="100"
+ android:visibility="gone" />
+
+ <TextView
+ android:id="@+id/textView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/progressBar"
+ android:layout_centerInParent="true"
+ android:background="@android:color/transparent"
+ android:text="@string/loading"
+ android:textColor="#FEFEFE"
+ android:visibility="gone" />
+
+</RelativeLayout>
diff --git a/android/app/src/main/res/mipmap/ic_launcher.png b/android/app/src/main/res/mipmap/ic_launcher.png
new file mode 100644
index 000000000..88a83782c
--- /dev/null
+++ b/android/app/src/main/res/mipmap/ic_launcher.png
Binary files differ
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..85238117f
--- /dev/null
+++ b/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="label">Minetest</string>
+ <string name="loading">Loading&#8230;</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>
+
+</resources>
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..291a4eaf1
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
+ <item name="windowActionBar">false</item>
+ <item name="android:windowFullscreen">true</item>
+ <item name="android:windowBackground">@drawable/bg</item>
+ <item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="p">shortEdges</item>
+ </style>
+
+ <style name="CustomProgressBar" parent="Widget.AppCompat.ProgressBar.Horizontal">
+ <item name="android:indeterminateOnly">false</item>
+ </style>
+
+</resources>