From a7143c2a8c48b234d78ec666193b942ae0b62ca3 Mon Sep 17 00:00:00 2001
From: NeroBurner <pyro4hell@gmail.com>
Date: Mon, 21 Jun 2021 21:51:42 +0200
Subject: Move build/android directory to root of project (#11283)

---
 android/app/src/main/AndroidManifest.xml           |  62 ++++++++
 .../java/net/minetest/minetest/CopyZipTask.java    |  82 ++++++++++
 .../java/net/minetest/minetest/CustomEditText.java |  45 ++++++
 .../java/net/minetest/minetest/GameActivity.java   | 174 +++++++++++++++++++++
 .../java/net/minetest/minetest/MainActivity.java   | 153 ++++++++++++++++++
 .../java/net/minetest/minetest/UnzipService.java   | 157 +++++++++++++++++++
 android/app/src/main/res/drawable/background.png   | Bin 0 -> 83 bytes
 android/app/src/main/res/drawable/bg.xml           |   4 +
 android/app/src/main/res/layout/activity_main.xml  |  30 ++++
 android/app/src/main/res/mipmap/ic_launcher.png    | Bin 0 -> 5780 bytes
 android/app/src/main/res/values/strings.xml        |  11 ++
 android/app/src/main/res/values/styles.xml         |  15 ++
 12 files changed, 733 insertions(+)
 create mode 100644 android/app/src/main/AndroidManifest.xml
 create mode 100644 android/app/src/main/java/net/minetest/minetest/CopyZipTask.java
 create mode 100644 android/app/src/main/java/net/minetest/minetest/CustomEditText.java
 create mode 100644 android/app/src/main/java/net/minetest/minetest/GameActivity.java
 create mode 100644 android/app/src/main/java/net/minetest/minetest/MainActivity.java
 create mode 100644 android/app/src/main/java/net/minetest/minetest/UnzipService.java
 create mode 100644 android/app/src/main/res/drawable/background.png
 create mode 100644 android/app/src/main/res/drawable/bg.xml
 create mode 100644 android/app/src/main/res/layout/activity_main.xml
 create mode 100644 android/app/src/main/res/mipmap/ic_launcher.png
 create mode 100644 android/app/src/main/res/values/strings.xml
 create mode 100644 android/app/src/main/res/values/styles.xml

(limited to 'android/app/src')

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
Binary files /dev/null and b/android/app/src/main/res/drawable/background.png 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
Binary files /dev/null and b/android/app/src/main/res/mipmap/ic_launcher.png 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>
-- 
cgit v1.2.3