File indexing completed on 2025-02-02 04:47:49

0001 /*
0002  * SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 package org.kde.kdeconnect.Helpers;
0008 
0009 import android.annotation.SuppressLint;
0010 import android.content.ContentResolver;
0011 import android.content.Context;
0012 import android.database.Cursor;
0013 import android.net.Uri;
0014 import android.provider.DocumentsContract;
0015 import android.provider.MediaStore;
0016 import android.provider.OpenableColumns;
0017 import android.util.Log;
0018 import android.webkit.MimeTypeMap;
0019 
0020 import org.apache.commons.io.FilenameUtils;
0021 import org.apache.commons.lang3.StringUtils;
0022 import org.kde.kdeconnect.NetworkPacket;
0023 
0024 import java.io.File;
0025 import java.io.InputStream;
0026 import java.util.Arrays;
0027 
0028 public class FilesHelper {
0029     public static final String LOG_TAG = "SendFileActivity";
0030 
0031     public static String getMimeTypeFromFile(String file) {
0032         String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(file));
0033         return StringUtils.defaultString(mime, "*/*");
0034     }
0035 
0036     public static String findNonExistingNameForNewFile(String path, String filename) {
0037         String name = FilenameUtils.getBaseName(filename);
0038         String ext = FilenameUtils.getExtension(filename);
0039 
0040         int num = 1;
0041         while (new File(path + "/" + filename).exists()) {
0042             filename = name + " (" + num + ")." + ext;
0043             num++;
0044         }
0045 
0046         return filename;
0047     }
0048 
0049     //Following code from http://activemq.apache.org/maven/5.7.0/kahadb/apidocs/src-html/org/apache/kahadb/util/IOHelper.html
0050 
0051     /**
0052      * Converts any string into a string that is safe to use as a file name.
0053      * The result will only include ascii characters and numbers, and the "-","_", and "." characters.
0054      */
0055     private static String toFileSystemSafeName(String name, boolean dirSeparators, int maxFileLength) {
0056         int size = name.length();
0057         StringBuilder rc = new StringBuilder(size * 2);
0058         for (int i = 0; i < size; i++) {
0059             char c = name.charAt(i);
0060             boolean valid = c >= 'a' && c <= 'z';
0061             valid = valid || (c >= 'A' && c <= 'Z');
0062             valid = valid || (c >= '0' && c <= '9');
0063             valid = valid || (c == '_') || (c == '-') || (c == '.');
0064             valid = valid || (dirSeparators && ((c == '/') || (c == '\\')));
0065 
0066             if (valid) {
0067                 rc.append(c);
0068             }
0069         }
0070         String result = rc.toString();
0071         if (result.length() > maxFileLength) {
0072             result = result.substring(result.length() - maxFileLength);
0073         }
0074         return result;
0075     }
0076 
0077     public static String toFileSystemSafeName(String name, boolean dirSeparators) {
0078         return toFileSystemSafeName(name, dirSeparators, 255);
0079     }
0080 
0081     public static String toFileSystemSafeName(String name) {
0082         return toFileSystemSafeName(name, true, 255);
0083     }
0084 
0085     private static int GetOpenFileCount() {
0086         return new File("/proc/self/fd").listFiles().length;
0087     }
0088 
0089     public static void LogOpenFileCount() {
0090         Log.e("KDE/FileCount", "" + GetOpenFileCount());
0091     }
0092 
0093 
0094     //Create the network packet from the URI
0095     public static NetworkPacket uriToNetworkPacket(final Context context, final Uri uri, String type) {
0096 
0097         try {
0098 
0099             ContentResolver cr = context.getContentResolver();
0100             InputStream inputStream = cr.openInputStream(uri);
0101 
0102             NetworkPacket np = new NetworkPacket(type);
0103 
0104             String filename = null;
0105             long size = -1;
0106             Long lastModified = null;
0107 
0108             if (uri.getScheme().equals("file")) {
0109                 // file:// is a non media uri, so we cannot query the ContentProvider
0110 
0111                 try {
0112                     File mFile = new File(uri.getPath());
0113 
0114                     filename = mFile.getName();
0115                     size = mFile.length();
0116                     lastModified = mFile.lastModified();
0117                 } catch (NullPointerException e) {
0118                     Log.e(LOG_TAG, "Received bad file URI", e);
0119                 }
0120 
0121             } else {
0122                 // Since we used Intent.CATEGORY_OPENABLE, these two columns are the only ones we are
0123                 // guaranteed to have: https://developer.android.com/reference/android/provider/OpenableColumns
0124                 String[] proj = {
0125                         OpenableColumns.SIZE,
0126                         OpenableColumns.DISPLAY_NAME,
0127                 };
0128 
0129                 try (Cursor cursor = cr.query(uri, proj, null, null, null)) {
0130                     int nameColumnIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME);
0131                     int sizeColumnIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE);
0132                     cursor.moveToFirst();
0133 
0134                     filename = cursor.getString(nameColumnIndex);
0135 
0136                     // It is recommended to check for the value to be null because there are
0137                     // situations were we don't know the size (for instance, if the file is
0138                     // not local to the device)
0139                     if (!cursor.isNull(sizeColumnIndex)) {
0140                         size = cursor.getLong(sizeColumnIndex);
0141                     }
0142 
0143                     lastModified = getLastModifiedTime(context, uri);
0144                 } catch (Exception e) {
0145                     Log.e(LOG_TAG, "Problem getting file information", e);
0146                 }
0147             }
0148 
0149             if (filename != null) {
0150                 np.set("filename", filename);
0151             } else {
0152                 // It would be very surprising if this happens
0153                 Log.e(LOG_TAG, "Unable to read filename");
0154             }
0155 
0156             if (lastModified != null) {
0157                 np.set("lastModified", lastModified);
0158             } else {
0159                 // This would not be too surprising, and probably means we need to improve
0160                 // FilesHelper.getLastModifiedTime
0161                 Log.w(LOG_TAG, "Unable to read file last modified time");
0162             }
0163 
0164             np.setPayload(new NetworkPacket.Payload(inputStream, size));
0165 
0166             return np;
0167         } catch (Exception e) {
0168             Log.e(LOG_TAG, "Exception creating network packet", e);
0169             return null;
0170         }
0171     }
0172 
0173     /**
0174      * By hook or by crook, get the last modified time of the passed content:// URI
0175      *
0176      * This is a challenge because different content sources have different columns defined, and
0177      * I don't know how to tell what the source of the content is.
0178      *
0179      * Therefore, my brilliant solution is to just try everything until something works.
0180      *
0181      * Will return null if nothing worked.
0182      */
0183     public static Long getLastModifiedTime(final Context context, final Uri uri) {
0184         ContentResolver cr = context.getContentResolver();
0185 
0186         Long lastModifiedTime = null;
0187 
0188         // Open a cursor without a column because we do not yet know what columns are defined
0189         try (Cursor cursor = cr.query(uri, null, null, null, null)) {
0190             if (cursor != null && cursor.moveToFirst()) {
0191                 String[] allColumns = cursor.getColumnNames();
0192 
0193                 // MediaStore.MediaColumns.DATE_MODIFIED resolves to "date_modified"
0194                 // I see this column defined in case we used the Gallery app to select the file to transfer
0195                 // This can occur both for devices running Storage Access Framework (SAF) if we select
0196                 // the Gallery to provide the file to transfer, as well as for older devices by doing the same
0197                 int mediaDataModifiedColumnIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED);
0198 
0199                 // DocumentsContract.Document.COLUMN_LAST_MODIFIED resolves to "last_modified"
0200                 // I see this column defined when, on a device using SAF we select a file using the
0201                 // file browser
0202                 // According to https://developer.android.com/reference/kotlin/android/provider/DocumentsContract
0203                 // all "document providers" must provide certain columns. Do we actually have a DocumentProvider here?
0204                 // I do not think this code path will ever happen for a non-media file is selected on
0205                 // an API < KitKat device, since those will be delivered as a file:// URI and handled
0206                 // accordingly. Therefore, it is safe to ignore the warning that this field requires
0207                 // API 19
0208                 @SuppressLint("InlinedApi")
0209                 int documentLastModifiedColumnIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED);
0210 
0211                 // If we have an image, it may be the case that MediaStore.MediaColumns.DATE_MODIFIED
0212                 // catches the modification date, but if not, here is another column we can look for.
0213                 // This should be checked *after* DATE_MODIFIED since I think that column might give
0214                 // better information
0215                 int imageDateTakenColumnIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);
0216 
0217                 // Report whether the captured timestamp is in milliseconds or seconds
0218                 // The truthy-ness of this value for each different type of column is known from either
0219                 // experimentation or the docs (when docs exist...)
0220                 boolean milliseconds;
0221 
0222                 int properColumnIndex;
0223                 if (mediaDataModifiedColumnIndex >= 0) {
0224                     properColumnIndex = mediaDataModifiedColumnIndex;
0225                     milliseconds = false;
0226                 } else if (documentLastModifiedColumnIndex >= 0) {
0227                     properColumnIndex = documentLastModifiedColumnIndex;
0228                     milliseconds = true;
0229                 } else if (imageDateTakenColumnIndex >= 0) {
0230                     properColumnIndex = imageDateTakenColumnIndex;
0231                     milliseconds = true;
0232                 } else {
0233                     // Nothing worked :(
0234                     String formattedColumns = Arrays.toString(allColumns);
0235                     Log.w("SendFileActivity", "Unable to get file modification time. Available columns were: " + formattedColumns);
0236                     return null;
0237                 }
0238 
0239                 if (!cursor.isNull(properColumnIndex)) {
0240                     lastModifiedTime = cursor.getLong(properColumnIndex);
0241                 }
0242 
0243                 if (!milliseconds) {
0244                     lastModifiedTime *= 1000;
0245                     milliseconds = true;
0246                 }
0247             }
0248         }
0249         return lastModifiedTime;
0250     }
0251 }