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 }