Shelter/app/src/main/java/net/typeblog/shelter/services/FileShuttleService.java
Peter Cai c716a22df8
FileShuttle: allow OPEN_DOCUMENT_TREE [WIP]
implement isChildDocument() for this.
2018-09-21 16:08:47 +08:00

292 lines
11 KiB
Java

package net.typeblog.shelter.services;
import android.app.Service;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.support.annotation.Nullable;
import android.webkit.MimeTypeMap;
import net.typeblog.shelter.ShelterApplication;
import net.typeblog.shelter.util.CrossProfileDocumentsProvider;
import net.typeblog.shelter.util.Utility;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// A service to forward file information across the profile boundary
public class FileShuttleService extends Service {
public static final long TIMEOUT = 10000;
// Periodic task to stop the service when idle.
// This service does not need to persist.
private Runnable mSuicideTask = this::suicide;
private Handler mHandler = new Handler(Looper.getMainLooper());
private IFileShuttleService.Stub mStub = new IFileShuttleService.Stub() {
@Override
public void ping() {
// Dummy method
resetSuicideTask();
}
@Override
public List<Map> loadFiles(String path) {
resetSuicideTask();
ArrayList<Map> ret = new ArrayList<>();
File f = new File(resolvePath(path));
if (f.listFiles() != null) {
for (File child : f.listFiles()) {
ret.add(loadFileMeta(child.getPath()));
}
}
return ret;
}
@Override
public Map loadFileMeta(String path) {
resetSuicideTask();
File f = new File(resolvePath(path));
HashMap<String, Object> map = new HashMap<>();
map.put(DocumentsContract.Document.COLUMN_DOCUMENT_ID, f.getAbsolutePath());
map.put(DocumentsContract.Document.COLUMN_DISPLAY_NAME, f.getName());
map.put(DocumentsContract.Document.COLUMN_SIZE, f.length());
map.put(DocumentsContract.Document.COLUMN_LAST_MODIFIED, f.lastModified());
if (f.isDirectory()) {
map.put(DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR);
map.put(DocumentsContract.Document.COLUMN_FLAGS,
DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE |
DocumentsContract.Document.FLAG_SUPPORTS_DELETE);
} else {
String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
Utility.getFileExtension(f.getAbsolutePath()));
int flags = DocumentsContract.Document.FLAG_SUPPORTS_DELETE;
if (mime != null && (mime.startsWith("image/") || mime.startsWith("video/"))) {
flags |= DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL;
}
if (mime == null) {
mime = "application/unknown";
}
map.put(DocumentsContract.Document.COLUMN_MIME_TYPE, mime);
map.put(DocumentsContract.Document.COLUMN_FLAGS, flags);
}
return map;
}
@Override
public ParcelFileDescriptor openFile(String path, String mode) {
resetSuicideTask();
File f = new File(resolvePath(path));
try {
return ParcelFileDescriptor.open(f, ParcelFileDescriptor.parseMode(mode));
} catch (FileNotFoundException e) {
return null;
}
}
@Override
public ParcelFileDescriptor openThumbnail(String path, Point sizeHint) {
resetSuicideTask();
String fullPath = resolvePath(path);
String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
Utility.getFileExtension(fullPath));
if (mime == null) {
return null;
}
if (mime.startsWith("image/")) {
// Image thumbnail
return loadImageThumbnail(fullPath, sizeHint);
} else if (mime.startsWith("video/")) {
// Video thumbnail
return loadVideoThumbnail(fullPath);
} else {
return null;
}
}
@Override
public String createFile(String path, String mimeType, String displayName) {
resetSuicideTask();
File f;
if (!DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
String fullPath = path + "/" + displayName;
String extensionPart = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
if (!fullPath.endsWith(extensionPart)) {
fullPath += extensionPart;
}
f = new File(resolvePath(fullPath));
if (mimeType.startsWith("image/") || mimeType.startsWith("video/")) {
// Notify the media scanner to scan the file
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(f));
sendBroadcast(intent);
}
try {
if (!f.createNewFile()) {
return null;
}
} catch (IOException e) {
return null;
}
} else {
String fullPath = path + "/" + displayName;
f = new File(resolvePath(fullPath));
if (!f.mkdir()) {
return null;
}
}
return f.getAbsolutePath();
}
@Override
public String deleteFile(String path) {
resetSuicideTask();
File f = new File(resolvePath(path));
f.delete();
return f.getParentFile().getAbsolutePath();
}
@Override
public boolean isChildOf(String parent, String child) {
File parentFile = new File(resolvePath(parent));
File childFile = new File(resolvePath(child));
String parentPath = parentFile.getAbsolutePath();
if (parentPath.charAt(parentPath.length() - 1) != '/') {
parentPath += "/"; // Make sure it ends with '/'
}
return parentFile.exists() && parentFile.isDirectory()
&& childFile.exists()
&& childFile.getAbsolutePath().startsWith(parentPath);
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
resetSuicideTask();
return mStub;
}
@Override
public void onDestroy() {
super.onDestroy();
android.util.Log.d("FileShuttleService", "being destroyed");
}
private String resolvePath(String path) {
if (path.startsWith(CrossProfileDocumentsProvider.DUMMY_ROOT)) {
return path.replaceFirst(CrossProfileDocumentsProvider.DUMMY_ROOT,
Environment.getExternalStorageDirectory().getAbsolutePath());
} else {
return path;
}
}
private void resetSuicideTask() {
mHandler.removeCallbacks(mSuicideTask);
mHandler.postDelayed(mSuicideTask, TIMEOUT);
}
private void suicide() {
mHandler.removeCallbacks(mSuicideTask);
((ShelterApplication) getApplication()).unbindFileShuttleService();
stopSelf();
}
private ParcelFileDescriptor loadImageThumbnail(String fullPath, Point sizeHint) {
int id = Utility.getMediaStoreId(FileShuttleService.this, fullPath);
if (id == -1) {
// Fallback to directly loading thumbnail from file
return loadBitmapThumbnail(fullPath, sizeHint);
}
Cursor result = MediaStore.Images.Thumbnails.queryMiniThumbnail(
getContentResolver(), id, MediaStore.Images.Thumbnails.MINI_KIND, null);
if (result.getCount() == 0) {
// If no thumbnail is found, we try to request one first
MediaStore.Images.Thumbnails.getThumbnail(
getContentResolver(), id, MediaStore.Images.Thumbnails.MINI_KIND, null);
result = MediaStore.Images.Thumbnails.queryMiniThumbnail(
getContentResolver(), id, MediaStore.Images.Thumbnails.MINI_KIND, null);
}
if (result.getCount() == 0) {
// Fallback to directly loading thumbnail from file
return loadBitmapThumbnail(fullPath, sizeHint);
} else {
result.moveToFirst();
try {
int index = result.getColumnIndex(MediaStore.Images.Thumbnails.DATA);
return getContentResolver().openFileDescriptor(
Uri.fromFile(new File(result.getString(index))), "r");
} catch (FileNotFoundException e) {
return null;
}
}
}
private ParcelFileDescriptor loadVideoThumbnail(String fullPath) {
// The MediaStore interface for video thumbnails just do not work at all
// It can't even retrieve video IDs from the database
// Anyway, use this as a temporary fix.
// TODO: Figure out how to use the MediaStore interface with videos
Bitmap bmp = ThumbnailUtils.createVideoThumbnail(fullPath, MediaStore.Video.Thumbnails.MINI_KIND);
return bitmapToFd(bmp);
}
// Fallback method for thumbnail loading: just load from disk, but load a scaled down version
private ParcelFileDescriptor loadBitmapThumbnail(String path, Point sizeHint) {
Bitmap bmp = Utility.decodeSampledBitmap(path, sizeHint.x, sizeHint.y);
if (bmp == null) {
return null;
}
return bitmapToFd(bmp);
}
private ParcelFileDescriptor bitmapToFd(Bitmap bmp) {
ParcelFileDescriptor[] pair;
try {
// Use a pipe as a virtual in-memory ParcelFileDescriptor
pair = ParcelFileDescriptor.createPipe();
} catch (IOException e) {
return null;
}
FileOutputStream os = new FileOutputStream(pair[1].getFileDescriptor());
// Send the bitmap into the pipe in another thread, so that we can return the
// reading fd to the Documents UI before we finish sending the Bitmap.
new Thread(() -> {
bmp.compress(Bitmap.CompressFormat.PNG, 100, os);
try {
os.flush();
os.close();
} catch (IOException e) {
// ...
}
bmp.recycle();
}).start();
return pair[0];
}
}