UnzipService.java 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. /*
  2. Minetest
  3. Copyright (C) 2014-2020 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua>
  4. Copyright (C) 2014-2020 ubulem, Bektur Mambetov <berkut87@gmail.com>
  5. This program is free software; you can redistribute it and/or modify
  6. it under the terms of the GNU Lesser General Public License as published by
  7. the Free Software Foundation; either version 2.1 of the License, or
  8. (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU Lesser General Public License for more details.
  13. You should have received a copy of the GNU Lesser General Public License along
  14. with this program; if not, write to the Free Software Foundation, Inc.,
  15. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  16. */
  17. package net.minetest.minetest;
  18. import android.app.IntentService;
  19. import android.app.Notification;
  20. import android.app.NotificationChannel;
  21. import android.app.NotificationManager;
  22. import android.app.PendingIntent;
  23. import android.content.Context;
  24. import android.content.Intent;
  25. import android.os.Build;
  26. import android.os.Environment;
  27. import android.util.Log;
  28. import androidx.annotation.NonNull;
  29. import androidx.annotation.Nullable;
  30. import androidx.annotation.StringRes;
  31. import java.io.File;
  32. import java.io.FileInputStream;
  33. import java.io.FileOutputStream;
  34. import java.io.IOException;
  35. import java.io.InputStream;
  36. import java.io.OutputStream;
  37. import java.util.zip.ZipEntry;
  38. import java.util.zip.ZipFile;
  39. import java.util.zip.ZipInputStream;
  40. public class UnzipService extends IntentService {
  41. public static final String ACTION_UPDATE = "net.minetest.minetest.UPDATE";
  42. public static final String ACTION_PROGRESS = "net.minetest.minetest.PROGRESS";
  43. public static final String ACTION_PROGRESS_MESSAGE = "net.minetest.minetest.PROGRESS_MESSAGE";
  44. public static final String ACTION_FAILURE = "net.minetest.minetest.FAILURE";
  45. public static final int SUCCESS = -1;
  46. public static final int FAILURE = -2;
  47. public static final int INDETERMINATE = -3;
  48. private final int id = 1;
  49. private NotificationManager mNotifyManager;
  50. private boolean isSuccess = true;
  51. private String failureMessage;
  52. private static boolean isRunning = false;
  53. public static synchronized boolean getIsRunning() {
  54. return isRunning;
  55. }
  56. private static synchronized void setIsRunning(boolean v) {
  57. isRunning = v;
  58. }
  59. public UnzipService() {
  60. super("net.minetest.minetest.UnzipService");
  61. }
  62. @Override
  63. protected void onHandleIntent(Intent intent) {
  64. Notification.Builder notificationBuilder = createNotification();
  65. final File zipFile = new File(getCacheDir(), "Minetest.zip");
  66. try {
  67. setIsRunning(true);
  68. File userDataDirectory = Utils.getUserDataDirectory(this);
  69. try (InputStream in = this.getAssets().open(zipFile.getName())) {
  70. try (OutputStream out = new FileOutputStream(zipFile)) {
  71. int readLen;
  72. byte[] readBuffer = new byte[16384];
  73. while ((readLen = in.read(readBuffer)) != -1) {
  74. out.write(readBuffer, 0, readLen);
  75. }
  76. }
  77. }
  78. migrate(notificationBuilder, userDataDirectory);
  79. unzip(notificationBuilder, zipFile, userDataDirectory);
  80. } catch (IOException e) {
  81. isSuccess = false;
  82. failureMessage = e.getLocalizedMessage();
  83. } finally {
  84. setIsRunning(false);
  85. if (!zipFile.delete()) {
  86. Log.w("UnzipService", "Minetest installation ZIP cannot be deleted");
  87. }
  88. }
  89. }
  90. private Notification.Builder createNotification() {
  91. String name = "net.minetest.minetest";
  92. String channelId = "Minetest channel";
  93. String description = "notifications from Minetest";
  94. Notification.Builder builder;
  95. if (mNotifyManager == null)
  96. mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
  97. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  98. int importance = NotificationManager.IMPORTANCE_LOW;
  99. NotificationChannel mChannel = null;
  100. if (mNotifyManager != null)
  101. mChannel = mNotifyManager.getNotificationChannel(channelId);
  102. if (mChannel == null) {
  103. mChannel = new NotificationChannel(channelId, name, importance);
  104. mChannel.setDescription(description);
  105. // Configure the notification channel, NO SOUND
  106. mChannel.setSound(null, null);
  107. mChannel.enableLights(false);
  108. mChannel.enableVibration(false);
  109. mNotifyManager.createNotificationChannel(mChannel);
  110. }
  111. builder = new Notification.Builder(this, channelId);
  112. } else {
  113. builder = new Notification.Builder(this);
  114. }
  115. Intent notificationIntent = new Intent(this, MainActivity.class);
  116. notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
  117. | Intent.FLAG_ACTIVITY_SINGLE_TOP);
  118. int pendingIntentFlag = 0;
  119. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
  120. pendingIntentFlag = PendingIntent.FLAG_MUTABLE;
  121. }
  122. PendingIntent intent = PendingIntent.getActivity(this, 0,
  123. notificationIntent, pendingIntentFlag);
  124. builder.setContentTitle(getString(R.string.notification_title))
  125. .setSmallIcon(R.mipmap.ic_launcher)
  126. .setContentText(getString(R.string.notification_description))
  127. .setContentIntent(intent)
  128. .setOngoing(true)
  129. .setProgress(0, 0, true);
  130. mNotifyManager.notify(id, builder.build());
  131. return builder;
  132. }
  133. private void unzip(Notification.Builder notificationBuilder, File zipFile, File userDataDirectory) throws IOException {
  134. int per = 0;
  135. int size;
  136. try (ZipFile zipSize = new ZipFile(zipFile)) {
  137. size = zipSize.size();
  138. }
  139. int readLen;
  140. byte[] readBuffer = new byte[16384];
  141. try (FileInputStream fileInputStream = new FileInputStream(zipFile);
  142. ZipInputStream zipInputStream = new ZipInputStream(fileInputStream)) {
  143. ZipEntry ze;
  144. while ((ze = zipInputStream.getNextEntry()) != null) {
  145. if (ze.isDirectory()) {
  146. ++per;
  147. Utils.createDirs(userDataDirectory, ze.getName());
  148. continue;
  149. }
  150. publishProgress(notificationBuilder, R.string.loading, 100 * ++per / size);
  151. try (OutputStream outputStream = new FileOutputStream(
  152. new File(userDataDirectory, ze.getName()))) {
  153. while ((readLen = zipInputStream.read(readBuffer)) != -1) {
  154. outputStream.write(readBuffer, 0, readLen);
  155. }
  156. }
  157. }
  158. }
  159. }
  160. void moveFileOrDir(@NonNull File src, @NonNull File dst) throws IOException {
  161. try {
  162. Process p = new ProcessBuilder("/system/bin/mv",
  163. src.getAbsolutePath(), dst.getAbsolutePath()).start();
  164. int exitCode = p.waitFor();
  165. if (exitCode != 0)
  166. throw new IOException("Move failed with exit code " + exitCode);
  167. } catch (InterruptedException e) {
  168. throw new IOException("Move operation interrupted");
  169. }
  170. }
  171. boolean recursivelyDeleteDirectory(@NonNull File loc) {
  172. try {
  173. Process p = new ProcessBuilder("/system/bin/rm", "-rf",
  174. loc.getAbsolutePath()).start();
  175. return p.waitFor() == 0;
  176. } catch (IOException | InterruptedException e) {
  177. return false;
  178. }
  179. }
  180. /**
  181. * Migrates user data from deprecated external storage to app scoped storage
  182. */
  183. private void migrate(Notification.Builder notificationBuilder, File newLocation) throws IOException {
  184. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
  185. return;
  186. }
  187. File oldLocation = new File(Environment.getExternalStorageDirectory(), "Minetest");
  188. if (!oldLocation.isDirectory())
  189. return;
  190. publishProgress(notificationBuilder, R.string.migrating, 0);
  191. if (!newLocation.mkdir()) {
  192. Log.e("UnzipService", "New installation folder cannot be made");
  193. }
  194. String[] dirs = new String[] { "worlds", "games", "mods", "textures", "client" };
  195. for (int i = 0; i < dirs.length; i++) {
  196. publishProgress(notificationBuilder, R.string.migrating, 100 * i / dirs.length);
  197. File dir = new File(oldLocation, dirs[i]), dir2 = new File(newLocation, dirs[i]);
  198. if (dir.isDirectory() && !dir2.isDirectory()) {
  199. moveFileOrDir(dir, dir2);
  200. }
  201. }
  202. for (String filename : new String[] { "minetest.conf" }) {
  203. File file = new File(oldLocation, filename), file2 = new File(newLocation, filename);
  204. if (file.isFile() && !file2.isFile()) {
  205. moveFileOrDir(file, file2);
  206. }
  207. }
  208. if (!recursivelyDeleteDirectory(oldLocation)) {
  209. Log.w("UnzipService", "Old installation files cannot be deleted successfully");
  210. }
  211. }
  212. private void publishProgress(@Nullable Notification.Builder notificationBuilder, @StringRes int message, int progress) {
  213. Intent intentUpdate = new Intent(ACTION_UPDATE);
  214. intentUpdate.putExtra(ACTION_PROGRESS, progress);
  215. intentUpdate.putExtra(ACTION_PROGRESS_MESSAGE, message);
  216. if (!isSuccess)
  217. intentUpdate.putExtra(ACTION_FAILURE, failureMessage);
  218. sendBroadcast(intentUpdate);
  219. if (notificationBuilder != null) {
  220. notificationBuilder.setContentText(getString(message));
  221. if (progress == INDETERMINATE) {
  222. notificationBuilder.setProgress(100, 50, true);
  223. } else {
  224. notificationBuilder.setProgress(100, progress, false);
  225. }
  226. mNotifyManager.notify(id, notificationBuilder.build());
  227. }
  228. }
  229. @Override
  230. public void onDestroy() {
  231. super.onDestroy();
  232. mNotifyManager.cancel(id);
  233. publishProgress(null, R.string.loading, isSuccess ? SUCCESS : FAILURE);
  234. }
  235. }