FileUtil.java 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. /* _____ _
  2. * |_ _| |_ _ _ ___ ___ _ __ __ _
  3. * | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. * |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. *
  6. * Threema for Android
  7. * Copyright (c) 2014-2021 Threema GmbH
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License, version 3,
  11. * as published by the Free Software Foundation.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. */
  21. package ch.threema.app.utils;
  22. import android.app.Activity;
  23. import android.content.ActivityNotFoundException;
  24. import android.content.ClipData;
  25. import android.content.ContentResolver;
  26. import android.content.ContentUris;
  27. import android.content.Context;
  28. import android.content.Intent;
  29. import android.content.pm.PackageManager;
  30. import android.content.pm.ResolveInfo;
  31. import android.database.Cursor;
  32. import android.net.Uri;
  33. import android.os.Build;
  34. import android.os.Environment;
  35. import android.provider.DocumentsContract;
  36. import android.provider.MediaStore;
  37. import android.text.TextUtils;
  38. import android.text.format.Formatter;
  39. import android.webkit.MimeTypeMap;
  40. import android.widget.Toast;
  41. import org.apache.commons.io.IOUtils;
  42. import org.slf4j.Logger;
  43. import org.slf4j.LoggerFactory;
  44. import java.io.File;
  45. import java.io.FileInputStream;
  46. import java.io.FileOutputStream;
  47. import java.io.IOException;
  48. import java.io.InputStream;
  49. import java.io.OutputStream;
  50. import java.text.SimpleDateFormat;
  51. import java.util.ArrayList;
  52. import java.util.List;
  53. import java.util.Locale;
  54. import androidx.annotation.NonNull;
  55. import androidx.annotation.Nullable;
  56. import androidx.annotation.WorkerThread;
  57. import androidx.fragment.app.Fragment;
  58. import ch.threema.app.R;
  59. import ch.threema.app.ThreemaApplication;
  60. import ch.threema.app.camera.CameraActivity;
  61. import ch.threema.app.filepicker.FilePickerActivity;
  62. import ch.threema.app.services.FileService;
  63. import ch.threema.app.ui.MediaItem;
  64. import ch.threema.storage.models.AbstractMessageModel;
  65. import ch.threema.storage.models.data.media.FileDataModel;
  66. import static ch.threema.app.ThreemaApplication.MAX_BLOB_SIZE;
  67. import static ch.threema.app.filepicker.FilePickerActivity.INTENT_DATA_DEFAULT_PATH;
  68. public class FileUtil {
  69. private static final Logger logger = LoggerFactory.getLogger(FileUtil.class);
  70. private FileUtil() {
  71. }
  72. public static boolean isFilePresent(File filename) {
  73. return filename != null && filename.exists() && filename.length() > 0;
  74. }
  75. public static void selectFile(Activity activity, Fragment fragment, String[] mimeTypes, int ID, boolean multi, int sizeLimit, String initialPath) {
  76. Intent intent;
  77. Context context;
  78. if (fragment != null) {
  79. context = fragment.getActivity();
  80. } else {
  81. context = activity;
  82. }
  83. if ((isMediaProviderSupported(context) && initialPath == null) || ConfigUtils.hasScopedStorage()) {
  84. intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
  85. intent.addCategory(Intent.CATEGORY_OPENABLE);
  86. if (mimeTypes.length > 1) {
  87. intent.setType("*/*");
  88. intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
  89. } else {
  90. intent.setType(mimeTypes[0]);
  91. }
  92. // undocumented APIs according to https://issuetracker.google.com/issues/72053350
  93. intent.putExtra("android.content.extra.SHOW_ADVANCED", true);
  94. intent.putExtra("android.content.extra.FANCY", true);
  95. intent.putExtra("android.content.extra.SHOW_FILESIZE", true);
  96. }
  97. else {
  98. intent = new Intent();
  99. if (MimeUtil.isVideoFile(mimeTypes[0]) || MimeUtil.isImageFile(mimeTypes[0])) {
  100. intent.setAction(Intent.ACTION_GET_CONTENT);
  101. } else {
  102. intent = new Intent(context, FilePickerActivity.class);
  103. if (initialPath != null) {
  104. intent.putExtra(INTENT_DATA_DEFAULT_PATH, initialPath);
  105. }
  106. }
  107. intent.setType(mimeTypes[0]);
  108. }
  109. if (multi) {
  110. intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
  111. }
  112. if (sizeLimit > 0) {
  113. intent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, (long) sizeLimit);
  114. }
  115. try {
  116. if (fragment != null) {
  117. fragment.startActivityForResult(intent, ID);
  118. } else {
  119. activity.startActivityForResult(intent, ID);
  120. }
  121. } catch (ActivityNotFoundException e) {
  122. Toast.makeText(context, R.string.no_activity_for_mime_type, Toast.LENGTH_LONG).show();
  123. }
  124. }
  125. public static boolean getCameraFile(Activity activity, Fragment fragment, File cameraFile, int requestCode, FileService fileService, boolean preferInternal) {
  126. try {
  127. Intent cameraIntent;
  128. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && preferInternal) {
  129. cameraIntent = new Intent(fragment != null ? fragment.getActivity() : activity, CameraActivity.class);
  130. cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, cameraFile.getCanonicalPath());
  131. cameraIntent.putExtra(CameraActivity.EXTRA_NO_VIDEO, true);
  132. } else {
  133. cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
  134. cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileService.getShareFileUri(cameraFile, null));
  135. cameraIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  136. }
  137. if (fragment != null) {
  138. fragment.startActivityForResult(cameraIntent, requestCode);
  139. } else {
  140. activity.startActivityForResult(cameraIntent, requestCode);
  141. }
  142. return true;
  143. } catch (Exception e) {
  144. logger.error("Exception", e);
  145. }
  146. return false;
  147. }
  148. public static void forwardMessages(Context context, Class<?> targetActivity, List<AbstractMessageModel> messageModels) {
  149. Intent intent = new Intent(context, targetActivity);
  150. intent.setAction(ThreemaApplication.INTENT_ACTION_FORWARD);
  151. intent.putExtra(ThreemaApplication.INTENT_DATA_IS_FORWARD, true);
  152. IntentDataUtil.appendMultiple(messageModels, intent);
  153. context.startActivity(intent);
  154. }
  155. public static @NonNull ArrayList<Uri> getUrisFromResult(@NonNull Intent intent, ContentResolver contentResolver) {
  156. Uri returnData = intent.getData();
  157. ClipData clipData = null;
  158. ArrayList<Uri> uriList = new ArrayList<>();
  159. clipData = intent.getClipData();
  160. if (clipData != null && clipData.getItemCount() > 0) {
  161. for (int i = 0; i < clipData.getItemCount(); i++) {
  162. ClipData.Item clipItem = clipData.getItemAt(i);
  163. if (clipItem != null) {
  164. Uri uri = clipItem.getUri();
  165. if (uri != null) {
  166. if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme())) {
  167. try {
  168. contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
  169. } catch (Exception e) {
  170. logger.error("Exception", e);
  171. }
  172. }
  173. uriList.add(uri);
  174. }
  175. }
  176. }
  177. } else {
  178. if (returnData != null) {
  179. if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(returnData.getScheme())) {
  180. try {
  181. contentResolver.takePersistableUriPermission(returnData, Intent.FLAG_GRANT_READ_URI_PERMISSION);
  182. } catch (Exception e) {
  183. logger.error("Exception", e);
  184. }
  185. }
  186. uriList.add(returnData);
  187. }
  188. }
  189. return validateUriList(uriList);
  190. }
  191. /**
  192. * Check if selected files are located within the app's private directory
  193. * @param uris Uris to check
  194. * @return List of Uris not located in the private directory
  195. */
  196. private static @NonNull ArrayList<Uri> validateUriList(ArrayList<Uri> uris) {
  197. String dataDir = Environment.getDataDirectory().toString();
  198. ArrayList<Uri> validatedUris = new ArrayList<>();
  199. if (uris != null && uris.size() > 0) {
  200. for (Uri uri : uris) {
  201. try {
  202. if (uri != null) {
  203. if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
  204. // Files from /data may not be sent if coming from a Picker
  205. final File f = new File(uri.getPath());
  206. final String filePath = f.getCanonicalPath();
  207. if (filePath.startsWith(dataDir)) {
  208. continue;
  209. }
  210. }
  211. validatedUris.add(uri);
  212. }
  213. } catch (Exception e) {
  214. //
  215. }
  216. }
  217. if (uris.size() != validatedUris.size()) {
  218. logger.debug("Error adding attachment");
  219. Toast.makeText(ThreemaApplication.getAppContext(), R.string.error_attaching_files, Toast.LENGTH_LONG).show();
  220. }
  221. }
  222. return validatedUris;
  223. }
  224. /**
  225. * Get the mime type by looking at the filename's extension
  226. * @param path filename or complete path of the file
  227. * @return Mime Type or application/octet-stream if a mime type could not be determined from the extension
  228. */
  229. @NonNull
  230. public static String getMimeTypeFromPath(@Nullable String path) {
  231. String mimeType = null;
  232. if (path != null) {
  233. String extension = MimeTypeMap.getFileExtensionFromUrl(path);
  234. if (TextUtils.isEmpty(extension)) {
  235. // getMimeTypeFromExtension() doesn't handle spaces in filenames nor can it handle
  236. // urlEncoded strings. Let's try one last time at finding the extension.
  237. int dotPos = path.lastIndexOf('.');
  238. if (0 <= dotPos) {
  239. extension = path.substring(dotPos + 1);
  240. }
  241. }
  242. if (!TextUtils.isEmpty(extension)) {
  243. mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase());
  244. }
  245. if (extension.equalsIgnoreCase("opus")) {
  246. // whatsapp ogg files
  247. mimeType = "audio/ogg";
  248. } else if (extension.equalsIgnoreCase("gpx")) {
  249. // https://issuetracker.google.com/issues/37120151
  250. mimeType = "application/gpx+xml";
  251. } else if (extension.equalsIgnoreCase("pkpass")) {
  252. mimeType = "application/vnd.apple.pkpass";
  253. }
  254. }
  255. if (TestUtil.empty(mimeType)) {
  256. return MimeUtil.MIME_TYPE_DEFAULT;
  257. }
  258. return mimeType;
  259. }
  260. @Nullable
  261. public static String getMimeTypeFromUri(@NonNull Context context, @Nullable Uri uri) {
  262. if (uri != null) {
  263. ContentResolver contentResolver = context.getContentResolver();
  264. String type = contentResolver.getType(uri);
  265. if (TestUtil.empty(type) || MimeUtil.MIME_TYPE_DEFAULT.equals(type)) {
  266. // path = FileUtil.getRealPathFromURI(context, uri);
  267. String filename = FileUtil.getFilenameFromUri(contentResolver, uri);
  268. return getMimeTypeFromPath(filename);
  269. }
  270. return type;
  271. }
  272. return null;
  273. }
  274. /*
  275. * Check if Storage Access Framework is really available
  276. */
  277. private static boolean isMediaProviderSupported(Context context) {
  278. final PackageManager pm = context.getPackageManager();
  279. // Pick up provider with action string
  280. final Intent i = new Intent(DocumentsContract.PROVIDER_INTERFACE);
  281. final List<ResolveInfo> providers = pm.queryIntentContentProviders(i, 0);
  282. for (ResolveInfo info : providers)
  283. {
  284. if(info != null && info.providerInfo != null)
  285. {
  286. final String authority = info.providerInfo.authority;
  287. if(isMediaDocument(Uri.parse(ContentResolver.SCHEME_CONTENT + "://" + authority)))
  288. return true;
  289. }
  290. }
  291. return false;
  292. }
  293. /*
  294. * Some content uri returned by systemUI file picker create intermittent permission problems
  295. * To fix this, we convert it in a file uri
  296. */
  297. public static Uri getFixedContentUri(Context context, Uri inUri) {
  298. if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
  299. if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(inUri.getScheme()) && inUri.toString().toUpperCase().contains("%3A")) {
  300. String path = getRealPathFromURI(context, inUri);
  301. if (!TestUtil.empty(path)) {
  302. File file = new File(path);
  303. if (file.exists()) {
  304. return Uri.fromFile(file);
  305. }
  306. }
  307. }
  308. }
  309. return inUri;
  310. }
  311. @Nullable
  312. public static String getRealPathFromURI(final Context context, final Uri uri) {
  313. // DocumentProvider
  314. if (DocumentsContract.isDocumentUri(context, uri)) {
  315. // ExternalStorageProvider
  316. if (isExternalStorageDocument(uri)) {
  317. final String docId = DocumentsContract.getDocumentId(uri);
  318. final String[] split = docId.split(":");
  319. final String type = split[0];
  320. if ("primary".equalsIgnoreCase(type)) {
  321. return Environment.getExternalStorageDirectory() + "/" + split[1];
  322. }
  323. }
  324. // DownloadsProvider
  325. else if (isDownloadsDocument(uri)) {
  326. final String id = DocumentsContract.getDocumentId(uri);
  327. if (id != null) {
  328. if (id.startsWith("raw:/")) {
  329. return id.substring(4);
  330. } else {
  331. try {
  332. final Uri contentUri = ContentUris.withAppendedId(
  333. Uri.parse(ContentResolver.SCHEME_CONTENT + "://downloads/public_downloads"), Long.parseLong(id));
  334. return getDataColumn(context, contentUri, null, null);
  335. } catch (NumberFormatException e) {
  336. logger.info("Unable to extract document ID. Giving up.");
  337. }
  338. }
  339. } else {
  340. logger.info("No document ID. Giving up.");
  341. }
  342. }
  343. // MediaProvider
  344. else if (isMediaDocument(uri)) {
  345. final String docId = DocumentsContract.getDocumentId(uri);
  346. final String[] split = docId.split(":");
  347. final String type = split[0];
  348. Uri contentUri = null;
  349. if ("image".equals(type)) {
  350. contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
  351. } else if ("video".equals(type)) {
  352. contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
  353. } else if ("audio".equals(type)) {
  354. contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
  355. }
  356. final String selection = "_id=?";
  357. final String[] selectionArgs = new String[]{
  358. split[1]
  359. };
  360. return getDataColumn(context, contentUri, selection, selectionArgs);
  361. }
  362. // MediaStore (and general)
  363. } else if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme())) {
  364. // Return the remote address
  365. if (isGooglePhotosUri(uri)) {
  366. return uri.getLastPathSegment();
  367. }
  368. return getDataColumn(context, uri, null, null);
  369. }
  370. // File
  371. else if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme())) {
  372. return uri.getPath();
  373. }
  374. return null;
  375. }
  376. private static boolean isExternalStorageDocument(Uri uri) {
  377. return "com.android.externalstorage.documents".equals(uri.getAuthority());
  378. }
  379. private static boolean isDownloadsDocument(Uri uri) {
  380. return "com.android.providers.downloads.documents".equals(uri.getAuthority());
  381. }
  382. private static boolean isMediaDocument(final Uri uri) {
  383. return "com.android.providers.media.documents".equals(uri.getAuthority());
  384. }
  385. private static boolean isGooglePhotosUri(Uri uri) {
  386. return "com.google.android.apps.photos.content".equals(uri.getAuthority());
  387. }
  388. @Nullable
  389. private static String getDataColumn(Context context, Uri uri, String selection,
  390. String[] selectionArgs) {
  391. String data = null;
  392. Cursor cursor = null;
  393. final String column = "_data";
  394. final String[] projection = {
  395. column
  396. };
  397. try {
  398. cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
  399. null);
  400. if (cursor != null && cursor.moveToFirst()) {
  401. final int column_index = cursor.getColumnIndexOrThrow(column);
  402. data = cursor.getString(column_index);
  403. }
  404. } catch (Exception e) {
  405. //
  406. } finally {
  407. if (cursor != null)
  408. cursor.close();
  409. }
  410. return data;
  411. }
  412. public static boolean isAnimGif(ContentResolver contentResolver, Uri uri) {
  413. if (uri == null) {
  414. return false;
  415. }
  416. byte[] buffer = new byte[4];
  417. try (InputStream is = contentResolver.openInputStream(uri)) {
  418. is.read(buffer);
  419. return isAnimGif(buffer);
  420. } catch (Exception x) {
  421. logger.error("Exception", x);
  422. return false;
  423. }
  424. }
  425. private static boolean isAnimGif(byte[] buffer) {
  426. return buffer != null
  427. && buffer.length >= 4
  428. && (buffer[0] == 0x47 && buffer[1] == 0x49 &&
  429. buffer[2] == 0x46 && buffer[3] == 0x38);
  430. }
  431. public static boolean isImageFile(FileDataModel fileDataModel) {
  432. return fileDataModel != null && (MimeUtil.isImageFile(fileDataModel.getMimeType()));
  433. }
  434. public static boolean isVideoFile(FileDataModel fileDataModel) {
  435. return fileDataModel != null && (MimeUtil.isVideoFile(fileDataModel.getMimeType()));
  436. }
  437. public static boolean isAudioFile(FileDataModel fileDataModel) {
  438. return fileDataModel != null && (MimeUtil.isAudioFile(fileDataModel.getMimeType()));
  439. }
  440. public static String getFileMessageDatePrefix(Context context, AbstractMessageModel messageModel, String fileType) {
  441. if (messageModel.getFileData() == null || messageModel.getFileData().getFileSize() == 0) {
  442. return "";
  443. }
  444. if (messageModel.getFileData().isDownloaded()) {
  445. return "";
  446. }
  447. if (fileType != null) {
  448. String datePrefixString = Formatter.formatShortFileSize(context, messageModel.getFileData().getFileSize());
  449. if (messageModel.isOutbox()) {
  450. datePrefixString = fileType + " | " + datePrefixString;
  451. } else {
  452. datePrefixString += " | " + fileType;
  453. }
  454. return datePrefixString;
  455. } else {
  456. return Formatter.formatShortFileSize(context, messageModel.getFileData().getFileSize());
  457. }
  458. }
  459. public static @NonNull String getMediaFilenamePrefix(@NonNull AbstractMessageModel messageModel) {
  460. SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.getDefault());
  461. return "threema-" + format.format(messageModel.getCreatedAt()) + "-" + messageModel.getApiMessageId();
  462. }
  463. public static @NonNull String getMediaFilenamePrefix() {
  464. SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd-HHmmssSSS", Locale.getDefault());
  465. return "threema-" + format.format(System.currentTimeMillis());
  466. }
  467. /**
  468. * Return a default filename keeping in account specified mime type
  469. * @param mimeType the mime type to generate a filename for
  470. * @return a filename with an extension
  471. */
  472. public static @NonNull String getDefaultFilename(@Nullable String mimeType) {
  473. if (TestUtil.empty(mimeType)) {
  474. mimeType = MimeUtil.MIME_TYPE_DEFAULT;
  475. }
  476. String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
  477. return getMediaFilenamePrefix() + "." + extension;
  478. }
  479. public static String sanitizeFileName(String filename) {
  480. if (!TestUtil.empty(filename)) {
  481. return filename.replaceAll("[:/*\"?|<>' ]", "_");
  482. }
  483. return null;
  484. }
  485. @WorkerThread
  486. public static boolean copyFile(@NonNull File source, @NonNull File dest) {
  487. try (InputStream inputStream = new FileInputStream(source);
  488. OutputStream outputStream = new FileOutputStream(dest))
  489. {
  490. IOUtils.copy(inputStream, outputStream);
  491. return true;
  492. } catch (Exception e) {
  493. logger.error("Exception", e);
  494. }
  495. return false;
  496. }
  497. @WorkerThread
  498. public static boolean copyFile(@NonNull Uri source, @NonNull File dest, @NonNull ContentResolver contentResolver) {
  499. try (InputStream inputStream = contentResolver.openInputStream(source);
  500. OutputStream outputStream = new FileOutputStream(dest))
  501. {
  502. if (inputStream != null) {
  503. IOUtils.copy(inputStream, outputStream);
  504. return true;
  505. }
  506. } catch (Exception e) {
  507. logger.error("Exception", e);
  508. }
  509. return false;
  510. }
  511. /**
  512. * Attempt to delete a file. If deleting fails, log a warning using the specified logger.
  513. *
  514. * Note: Do not use this if error recovery is important!
  515. *
  516. * @param file The file that should be deleted
  517. * @param description The description of the file (e.g. "message queue database")
  518. * @param logger The logger to use
  519. */
  520. public static void deleteFileOrWarn(
  521. @NonNull File file,
  522. @Nullable String description,
  523. @NonNull Logger logger
  524. ) {
  525. if (!file.delete()) {
  526. logger.warn("Could not delete {}", description);
  527. }
  528. }
  529. /**
  530. * See {@link #deleteFileOrWarn(File, String, Logger)}
  531. */
  532. public static void deleteFileOrWarn(
  533. @NonNull String path,
  534. @Nullable String description,
  535. @NonNull Logger logger
  536. ) {
  537. FileUtil.deleteFileOrWarn(new File(path), description, logger);
  538. }
  539. /**
  540. * Create a new file or re-use existing file. Log if file already exists.
  541. * @param file The file that should be created or re-used
  542. * @param logger The logger facility to use
  543. */
  544. public static void createNewFileOrLog(
  545. @NonNull File file,
  546. @NonNull Logger logger
  547. ) throws IOException {
  548. if (!file.createNewFile()) {
  549. logger.debug("File {} already exists", file.getAbsolutePath());
  550. }
  551. }
  552. /**
  553. * Try to generated a File with the given filename in the given path
  554. * If a file of the same name exists, add a number to the filename (possibly between name and extension)
  555. * @param destPath Destination path
  556. * @param destFilename Desired filename
  557. * @return File object
  558. */
  559. public static File getUniqueFile(String destPath, String destFilename) {
  560. File destFile = new File(destPath, destFilename);
  561. String extension = MimeTypeMap.getFileExtensionFromUrl(destFilename);
  562. if (!TestUtil.empty(extension)) {
  563. extension = "." + extension;
  564. }
  565. String filePart = destFilename.substring(0, destFilename.length() - extension.length());
  566. int i = 0;
  567. while (destFile.exists()) {
  568. i++;
  569. destFile = new File(destPath, filePart + " (" + i + ")" + extension);
  570. if (!destFile.exists()) {
  571. break;
  572. }
  573. }
  574. return destFile;
  575. }
  576. /**
  577. * Returns the filename of the object referred to by mediaItem. If no filename can be found, generate one
  578. * @param contentResolver ContentResolver
  579. * @param mediaItem MediaItem representing the source file
  580. * @return A filename
  581. */
  582. public static @NonNull String getFilenameFromUri(@NonNull ContentResolver contentResolver, @NonNull MediaItem mediaItem) {
  583. String filename = getFilenameFromUri(contentResolver, mediaItem.getUri());
  584. if (TextUtils.isEmpty(filename)) {
  585. filename = getDefaultFilename(mediaItem.getMimeType());
  586. }
  587. return filename;
  588. }
  589. /**
  590. * Returns the filename of the object referred to by uri by querying the content resolver
  591. * @param contentResolver ContentResolver
  592. * @param uri Uri pointing at the object
  593. * @return A filename or null if none is found
  594. */
  595. @Nullable
  596. public static String getFilenameFromUri(ContentResolver contentResolver, Uri uri) {
  597. String filename = null;
  598. if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(uri.getScheme())) {
  599. filename = uri.getLastPathSegment();
  600. } else {
  601. try (final Cursor cursor = contentResolver.query(uri, null, null, null, null)) {
  602. if (cursor != null && cursor.moveToNext()) {
  603. filename = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME));
  604. }
  605. } catch (Exception e) {
  606. logger.error("Unable to query Content Resolver", e);
  607. }
  608. }
  609. return filename;
  610. }
  611. /**
  612. * Try to get a file uri from a content uri to maintain access to a file across two activities.
  613. * NOTE: This hack will probably stop working in API 30
  614. * @param uri content uri to resolve
  615. * @return file uri, if a file path could be resolved
  616. */
  617. public static Uri getFileUri(Uri uri) {
  618. String path = FileUtil.getRealPathFromURI(ThreemaApplication.getAppContext(), uri);
  619. if (path != null) {
  620. File file = new File(path);
  621. if (file.canRead()) {
  622. return Uri.fromFile(file);
  623. }
  624. }
  625. return uri;
  626. }
  627. /**
  628. * Select a file from a gallery app. Shows a selector first to allow for choosing the desired gallery app or SystemUIs file picker.
  629. * Does not necessarily need file permissions as a modern gallery app will return a content Uri with a temporary permission to access the file
  630. * @param activity Activity where the result of the selection should end up
  631. * @param fragment Fragment where the result of the selection should end up
  632. * @param requestCode Request code to use for result
  633. * @param includeVideo Whether to include the possibility to select video files (if supported by app)
  634. */
  635. public static void selectFromGallery(@Nullable Activity activity, @Nullable Fragment fragment, int requestCode, boolean includeVideo) {
  636. if (activity == null) {
  637. activity = fragment.getActivity();
  638. }
  639. try {
  640. Intent startIntent;
  641. Intent getContentIntent = new Intent();
  642. getContentIntent.setType(includeVideo ? MimeUtil.MIME_TYPE_VIDEO: MimeUtil.MIME_TYPE_IMAGE);
  643. getContentIntent.setAction(Intent.ACTION_GET_CONTENT);
  644. getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);
  645. getContentIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
  646. getContentIntent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, MAX_BLOB_SIZE);
  647. if (includeVideo) {
  648. Intent pickIntent = new Intent(Intent.ACTION_PICK);
  649. pickIntent.setType(MimeUtil.MIME_TYPE_IMAGE);
  650. startIntent = Intent.createChooser(pickIntent, activity.getString(R.string.select_from_gallery));
  651. startIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[]{getContentIntent});
  652. } else {
  653. startIntent = getContentIntent;
  654. }
  655. if (fragment != null) {
  656. fragment.startActivityForResult(startIntent, requestCode);
  657. } else {
  658. activity.startActivityForResult(startIntent, requestCode);
  659. }
  660. } catch (Exception e) {
  661. logger.debug("Exception", e);
  662. Toast.makeText(activity, R.string.no_activity_for_mime_type, Toast.LENGTH_SHORT).show();
  663. }
  664. }
  665. }