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