forked from PojavLauncherTeam/PojavLauncher
-
Notifications
You must be signed in to change notification settings - Fork 244
Expand file tree
/
Copy pathFolderProvider.java
More file actions
381 lines (339 loc) · 15.8 KB
/
Copy pathFolderProvider.java
File metadata and controls
381 lines (339 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
package net.kdt.pojavlaunch.scoped;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Point;
import android.net.Uri;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.util.Log;
import android.webkit.MimeTypeMap;
import androidx.annotation.Nullable;
import net.kdt.pojavlaunch.BuildConfig;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* A document provider for the Storage Access Framework which exposes the files in the
* $HOME/ directory to other apps.
* <p/>
* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
* <p/>
* "A document provider and ACTION_GET_CONTENT should be considered mutually exclusive. If you
* support both of them simultaneously, your app will appear twice in the system picker UI,
* offering two different ways of accessing your stored data. This would be confusing for users."
* - <a href="http://developer.android.com/guide/topics/providers/document-provider.html#43">...</a>
*/
public class FolderProvider extends DocumentsProvider {
private static final List<String> BLOCKED_PACKAGES = List.of("com.dnamobile.modlymodmanager");
private static final String ALL_MIME_TYPES = "*/*";
private File BASE_DIR;
private ContentResolver mContentResolver;
private String mStorageProviderAuthortiy;
// The default columns to return information about a root if no specific
// columns are requested in a query.
private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{
Root.COLUMN_ROOT_ID,
Root.COLUMN_MIME_TYPES,
Root.COLUMN_FLAGS,
Root.COLUMN_ICON,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_AVAILABLE_BYTES
};
// The default columns to return information about a document if no specific
// columns are requested in a query.
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS,
Document.COLUMN_SIZE
};
@Override
public Cursor queryRoots(String[] projection) {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
final String applicationName = getContext().getString(R.string.app_short_name);
String summary = BuildConfig.VERSION_NAME;
if (BuildConfig.DEBUG) {
summary = "(" + getContext().getString(R.string.generic_debug) + ") " + summary;
}
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
row.add(Root.COLUMN_SUMMARY, summary);
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD);
row.add(Root.COLUMN_TITLE, applicationName);
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
return result;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
if(BLOCKED_PACKAGES.contains(getCallingPackage()))
throw new RuntimeException("Unsupported package caller!");
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
// Future-proofing in case if we implement realtime file watching
result.setNotificationUri(mContentResolver, createUriForDocId(documentId));
includeFile(result, documentId, null);
return result;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
if(BLOCKED_PACKAGES.contains(getCallingPackage()))
throw new RuntimeException("Unsupported package caller!");
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(parentDocumentId);
final File[] children = parent.listFiles();
if(children == null) throw new FileNotFoundException("Unable to list files in "+parent.getAbsolutePath());
for (File file : children) {
includeFile(result, null, file);
}
// Set the notification URI as that's what the "Files" app will be listening to in case of file deletion
result.setNotificationUri(mContentResolver, createUriForDocId(parentDocumentId));
return result;
}
@Override
public ParcelFileDescriptor openDocument(final String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final int accessMode = ParcelFileDescriptor.parseMode(mode);
return ParcelFileDescriptor.open(file, accessMode);
}
@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return new AssetFileDescriptor(pfd, 0, file.length());
}
@Override
public boolean onCreate() {
if (Tools.checkStorageRoot(getContext())) {
Tools.initStorageConstants(getContext());
} else {
return false;
}
BASE_DIR = new File(Tools.DIR_GAME_HOME);
mContentResolver = getContext().getContentResolver();
mStorageProviderAuthortiy = getContext().getString(R.string.storageProviderAuthorities);
return true;
}
@Override
public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
if(BLOCKED_PACKAGES.contains(getCallingPackage()))
throw new RuntimeException("Unsupported package caller!");
File newFile = new File(parentDocumentId, displayName);
int noConflictId = 2;
while (newFile.exists()) {
newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")");
}
try {
boolean succeeded;
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
succeeded = newFile.mkdir();
} else {
succeeded = newFile.createNewFile();
}
if (!succeeded) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
} catch (IOException e) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
// Notify the file manager that the parent directory has changed
notifyChange(createUriForDocId(parentDocumentId));
return newFile.getPath();
}
@Override
public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
File sourceFile = getFileForDocId(documentId);
File sourceParent = sourceFile.getParentFile();
if(sourceParent == null) throw new FileNotFoundException("Cannot rename root");
File targetFile = new File(getDocIdForFile(sourceParent) + "/" + displayName);
if(!sourceFile.renameTo(targetFile)){
throw new FileNotFoundException("Couldn't rename the document with id" + documentId);
}
return getDocIdForFile(targetFile);
}
@Override
public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) throws FileNotFoundException {
File sourceFile = getFileForDocId(sourceParentDocumentId + sourceDocumentId);
File targetFile = new File(targetParentDocumentId + sourceDocumentId);
if(!sourceFile.renameTo(targetFile)){
throw new FileNotFoundException("Failed to move the document with id " + sourceFile.getPath());
}
return getDocIdForFile(targetFile);
}
@Override
public void removeDocument(String documentId, String parentDocumentId) throws FileNotFoundException {
deleteDocument(parentDocumentId + "/" + documentId);
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
if(file.isDirectory()){
try {
FileUtils.deleteDirectory(file);
} catch (IOException e) {
throw new FileNotFoundException("Failed to delete document with id " + documentId);
}
}else{
if (!file.delete()) {
throw new FileNotFoundException("Failed to delete document with id " + documentId);
}
}
// Notify the file manager that the parent directory has changed
notifyChange(createUriForFile(file.getParentFile()));
}
@Override
public String getDocumentType(String documentId) throws FileNotFoundException {
Log.i("FolderPRovider", "getDocumentType("+documentId+")");
File file = getFileForDocId(documentId);
return getMimeType(file);
}
@Override
public Cursor querySearchDocuments(String rootId, String query, String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(rootId);
// This example implementation searches file names for the query and doesn't rank search
// results, so we can stop as soon as we find a sufficient number of matches. Other
// implementations might rank results and use other data about files, rather than the file
// name, to produce a match.
final LinkedList<File> pending = new LinkedList<>();
pending.add(parent);
final int MAX_SEARCH_RESULTS = 50;
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
final File file = pending.removeFirst();
// Avoid directories outside the $HOME directory linked with symlinks (to avoid e.g. search
// through the whole SD card).
boolean isInsideHome;
try {
isInsideHome = file.getCanonicalPath().startsWith(Tools.DIR_GAME_HOME);
} catch (IOException e) {
isInsideHome = true;
}
if (isInsideHome) {
if (file.isDirectory()) {
File[] listing = file.listFiles();
if(listing != null) Collections.addAll(pending, listing);
} else {
if (file.getName().toLowerCase().contains(query)) {
includeFile(result, null, file);
}
}
}
}
return result;
}
@Override
public boolean isChildDocument(String parentDocumentId, String documentId) {
return documentId.startsWith(parentDocumentId);
}
/**
* Get the document id given a file. This document id must be consistent across time as other
* applications may save the ID and use it to reference documents later.
* <p/>
* The reverse of @{link #getFileForDocId}.
*/
private static String getDocIdForFile(File file) {
return file.getAbsolutePath();
}
/**
* Get the file given a document id (the reverse of {@link #getDocIdForFile(File)}).
*/
private static File getFileForDocId(String docId) throws FileNotFoundException {
final File f = new File(docId);
if (!f.exists()) throw new FileNotFoundException(f.getAbsolutePath() + " not found");
return f;
}
private static String getMimeType(File file) {
if (file.isDirectory()) {
return Document.MIME_TYPE_DIR;
} else {
final String name = file.getName();
final int lastDot = name.lastIndexOf('.');
if (lastDot >= 0) {
final String extension = name.substring(lastDot + 1).toLowerCase();
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) return mime;
}
return "application/octet-stream";
}
}
/**
* Add a representation of a file to a cursor.
*
* @param result the cursor to modify
* @param docId the document ID representing the desired file (may be null if given file)
* @param file the File object representing the desired file (may be null if given docID)
*/
private void includeFile(MatrixCursor result, String docId, File file)
throws FileNotFoundException {
if (docId == null) {
docId = getDocIdForFile(file);
} else {
file = getFileForDocId(docId);
}
int flags = 0;
if (file.isDirectory()) {
if (file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else if (file.canWrite()) {
flags |= Document.FLAG_SUPPORTS_WRITE;
}
File parent = file.getParentFile();
if(parent != null) { // Only fails in one case: when the parent is /, which you can't delete.
if(parent.canWrite()) flags |= Document.FLAG_SUPPORTS_DELETE;
}
final String displayName = file.getName();
final String mimeType = getMimeType(file);
if (mimeType.startsWith("image/")) flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
row.add(Document.COLUMN_SIZE, file.length());
row.add(Document.COLUMN_MIME_TYPE, mimeType);
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
row.add(Document.COLUMN_FLAGS, flags);
row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher);
}
@Override
@TargetApi(26)
public DocumentsContract.Path findDocumentPath(@Nullable String parentDocumentId, String childDocumentId) throws FileNotFoundException {
File source = BASE_DIR;
if(parentDocumentId != null) source = getFileForDocId(parentDocumentId);
File destination = getFileForDocId(childDocumentId);
List<String> pathIds = new ArrayList<>();
while(!source.equals(destination) && destination != null) {
pathIds.add(getDocIdForFile(destination));
destination = destination.getParentFile();
}
pathIds.add(getDocIdForFile(source));
Collections.reverse(pathIds);
Log.i("FolderProvider", pathIds.toString());
return new DocumentsContract.Path(getDocIdForFile(source), pathIds);
}
private Uri createUriForDocId(String documentId) throws FileNotFoundException {
return createUriForFile(getFileForDocId(documentId));
}
private Uri createUriForFile(File file) {
return DocumentsContract.buildDocumentUri(mStorageProviderAuthortiy, file.getAbsolutePath());
}
private void notifyChange(Uri uri) {
mContentResolver.notifyChange(uri, null);
}
}