• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2008-2009, Motorola, Inc.
3  *
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *
9  * - Redistributions of source code must retain the above copyright notice,
10  * this list of conditions and the following disclaimer.
11  *
12  * - Redistributions in binary form must reproduce the above copyright notice,
13  * this list of conditions and the following disclaimer in the documentation
14  * and/or other materials provided with the distribution.
15  *
16  * - Neither the name of the Motorola, Inc. nor the names of its contributors
17  * may be used to endorse or promote products derived from this software
18  * without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30  * POSSIBILITY OF SUCH DAMAGE.
31  */
32 
33 package com.android.bluetooth.opp;
34 
35 import android.app.NotificationManager;
36 import android.bluetooth.BluetoothAdapter;
37 import android.bluetooth.BluetoothDevice;
38 import android.bluetooth.BluetoothProfile;
39 import android.bluetooth.BluetoothProtoEnums;
40 import android.content.ActivityNotFoundException;
41 import android.content.ComponentName;
42 import android.content.ContentResolver;
43 import android.content.ContentValues;
44 import android.content.Context;
45 import android.content.Intent;
46 import android.content.pm.PackageManager;
47 import android.content.pm.ResolveInfo;
48 import android.database.Cursor;
49 import android.icu.text.MessageFormat;
50 import android.net.Uri;
51 import android.os.Environment;
52 import android.os.ParcelFileDescriptor;
53 import android.os.SystemProperties;
54 import android.provider.Settings;
55 import android.util.EventLog;
56 import android.util.Log;
57 
58 import com.android.bluetooth.BluetoothMethodProxy;
59 import com.android.bluetooth.BluetoothStatsLog;
60 import com.android.bluetooth.R;
61 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils;
62 import com.android.internal.annotations.VisibleForTesting;
63 
64 import java.io.File;
65 import java.io.IOException;
66 import java.math.RoundingMode;
67 import java.text.DecimalFormat;
68 import java.util.ArrayList;
69 import java.util.Arrays;
70 import java.util.HashMap;
71 import java.util.List;
72 import java.util.Locale;
73 import java.util.Map;
74 import java.util.Objects;
75 import java.util.concurrent.ConcurrentHashMap;
76 
77 /** This class has some utilities for Opp application; */
78 // Next tag value for ContentProfileErrorReportUtils.report(): 10
79 public class BluetoothOppUtility {
80     private static final String TAG = BluetoothOppUtility.class.getSimpleName();
81 
82     // TODO(b/398120192): use API instead of a hardcode string.
83     private static final String NEARBY_SHARING_COMPONENT = "nearby_sharing_component";
84 
85     /** Whether the device has the "nosdcard" characteristic, or null if not-yet-known. */
86     private static Boolean sNoSdCard = null;
87 
88     @VisibleForTesting
89     static final ConcurrentHashMap<Uri, BluetoothOppSendFileInfo> sSendFileMap =
90             new ConcurrentHashMap<Uri, BluetoothOppSendFileInfo>();
91 
isBluetoothShareUri(Uri uri)92     public static boolean isBluetoothShareUri(Uri uri) {
93         if (uri.toString().startsWith(BluetoothShare.CONTENT_URI.toString())
94                 && !uri.getAuthority().equals(BluetoothShare.CONTENT_URI.getAuthority())) {
95             EventLog.writeEvent(0x534e4554, "225880741", -1, "");
96         }
97         return Objects.equals(uri.getAuthority(), BluetoothShare.CONTENT_URI.getAuthority());
98     }
99 
queryRecord(Context context, Uri uri)100     public static BluetoothOppTransferInfo queryRecord(Context context, Uri uri) {
101         BluetoothOppTransferInfo info = new BluetoothOppTransferInfo();
102         Cursor cursor =
103                 BluetoothMethodProxy.getInstance()
104                         .contentResolverQuery(
105                                 context.getContentResolver(), uri, null, null, null, null);
106         if (cursor != null) {
107             if (cursor.moveToFirst()) {
108                 fillRecord(context, cursor, info);
109             }
110             cursor.close();
111         } else {
112             info = null;
113             Log.v(TAG, "BluetoothOppManager Error: not got data from db for uri:" + uri);
114         }
115         return info;
116     }
117 
fillRecord(Context context, Cursor cursor, BluetoothOppTransferInfo info)118     public static void fillRecord(Context context, Cursor cursor, BluetoothOppTransferInfo info) {
119         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
120         info.mID = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare._ID));
121         info.mStatus = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.STATUS));
122         info.mDirection = cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.DIRECTION));
123         info.mTotalBytes = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TOTAL_BYTES));
124         info.mCurrentBytes =
125                 cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.CURRENT_BYTES));
126         info.mTimeStamp = cursor.getLong(cursor.getColumnIndexOrThrow(BluetoothShare.TIMESTAMP));
127         info.mDestAddr = cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.DESTINATION));
128 
129         info.mFileName = cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare._DATA));
130         if (info.mFileName == null) {
131             info.mFileName =
132                     cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.FILENAME_HINT));
133         }
134         if (info.mFileName == null) {
135             info.mFileName = context.getString(R.string.unknown_file);
136         }
137 
138         info.mFileUri = cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.URI));
139 
140         if (info.mFileUri != null) {
141             Uri u = Uri.parse(info.mFileUri);
142             info.mFileType = context.getContentResolver().getType(u);
143         } else {
144             Uri u = Uri.parse(info.mFileName);
145             info.mFileType = context.getContentResolver().getType(u);
146         }
147         if (info.mFileType == null) {
148             info.mFileType =
149                     cursor.getString(cursor.getColumnIndexOrThrow(BluetoothShare.MIMETYPE));
150         }
151 
152         BluetoothDevice remoteDevice = adapter.getRemoteDevice(info.mDestAddr);
153         info.mDeviceName = BluetoothOppManager.getInstance(context).getDeviceName(remoteDevice);
154 
155         int confirmationType =
156                 cursor.getInt(cursor.getColumnIndexOrThrow(BluetoothShare.USER_CONFIRMATION));
157         info.mHandoverInitiated =
158                 confirmationType == BluetoothShare.USER_CONFIRMATION_HANDOVER_CONFIRMED;
159 
160         Log.v(TAG, "Get data from db:" + info.mFileName + info.mFileType + info.mDestAddr);
161     }
162 
163     /** Organize Array list for transfers in one batch */
164     // This function is used when UI show batch transfer. Currently only show single transfer.
queryTransfersInBatch(Context context, Long timeStamp)165     public static List<String> queryTransfersInBatch(Context context, Long timeStamp) {
166         List<String> uris = new ArrayList<>();
167         final String where = BluetoothShare.TIMESTAMP + " == " + timeStamp;
168         Cursor metadataCursor =
169                 BluetoothMethodProxy.getInstance()
170                         .contentResolverQuery(
171                                 context.getContentResolver(),
172                                 BluetoothShare.CONTENT_URI,
173                                 new String[] {BluetoothShare._DATA},
174                                 where,
175                                 null,
176                                 BluetoothShare._ID);
177 
178         if (metadataCursor == null) {
179             return null;
180         }
181 
182         for (metadataCursor.moveToFirst();
183                 !metadataCursor.isAfterLast();
184                 metadataCursor.moveToNext()) {
185             String fileName = metadataCursor.getString(0);
186             Uri path = Uri.parse(fileName);
187             // If there is no scheme, then it must be a file
188             if (path.getScheme() == null) {
189                 path = Uri.fromFile(new File(fileName));
190             }
191             uris.add(path.toString());
192             Log.v(TAG, "Uri in this batch: " + path.toString());
193         }
194         metadataCursor.close();
195         return uris;
196     }
197 
198     /**
199      * Open the received file with appropriate application, if can not find application to handle,
200      * display error dialog.
201      */
openReceivedFile( Context context, String fileName, String mimetype, Long timeStamp, Uri uri)202     public static void openReceivedFile(
203             Context context, String fileName, String mimetype, Long timeStamp, Uri uri) {
204         if (fileName == null || mimetype == null) {
205             Log.e(TAG, "ERROR: Para fileName ==null, or mimetype == null");
206             ContentProfileErrorReportUtils.report(
207                     BluetoothProfile.OPP,
208                     BluetoothProtoEnums.BLUETOOTH_OPP_UTILITY,
209                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
210                     0);
211             return;
212         }
213 
214         if (!isBluetoothShareUri(uri)) {
215             Log.e(TAG, "Trying to open a file that wasn't transferred over Bluetooth");
216             ContentProfileErrorReportUtils.report(
217                     BluetoothProfile.OPP,
218                     BluetoothProtoEnums.BLUETOOTH_OPP_UTILITY,
219                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
220                     1);
221             return;
222         }
223 
224         Uri path = null;
225         Cursor metadataCursor =
226                 BluetoothMethodProxy.getInstance()
227                         .contentResolverQuery(
228                                 context.getContentResolver(),
229                                 uri,
230                                 new String[] {BluetoothShare.URI},
231                                 null,
232                                 null,
233                                 null);
234         if (metadataCursor != null) {
235             try {
236                 if (metadataCursor.moveToFirst()) {
237                     path = Uri.parse(metadataCursor.getString(0));
238                 }
239             } finally {
240                 metadataCursor.close();
241             }
242         }
243 
244         if (path == null) {
245             Log.e(TAG, "file uri not exist");
246             ContentProfileErrorReportUtils.report(
247                     BluetoothProfile.OPP,
248                     BluetoothProtoEnums.BLUETOOTH_OPP_UTILITY,
249                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
250                     2);
251             return;
252         }
253 
254         if (!fileExists(context, path)) {
255             Intent in = new Intent(context, BluetoothOppBtErrorActivity.class);
256             in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
257             in.putExtra("title", context.getString(R.string.not_exist_file));
258             in.putExtra("content", context.getString(R.string.not_exist_file_desc));
259             context.startActivity(in);
260 
261             // Due to the file is not existing, delete related info in btopp db
262             // to prevent this file from appearing in live folder
263             Log.v(TAG, "This uri will be deleted: " + uri);
264             BluetoothMethodProxy.getInstance()
265                     .contentResolverDelete(context.getContentResolver(), uri, null, null);
266             return;
267         }
268 
269         if (isRecognizedFileType(context, path, mimetype)) {
270             Intent activityIntent = new Intent(Intent.ACTION_VIEW);
271             activityIntent.setDataAndType(
272                     path.normalizeScheme(), Intent.normalizeMimeType(mimetype));
273             activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
274             activityIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
275 
276             try {
277                 Log.v(TAG, "ACTION_VIEW intent sent out: " + path + " / " + mimetype);
278                 context.startActivity(activityIntent);
279             } catch (ActivityNotFoundException ex) {
280                 ContentProfileErrorReportUtils.report(
281                         BluetoothProfile.OPP,
282                         BluetoothProtoEnums.BLUETOOTH_OPP_UTILITY,
283                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
284                         3);
285                 Log.v(TAG, "no activity for handling ACTION_VIEW intent:  " + mimetype, ex);
286             }
287         } else {
288             Intent in = new Intent(context, BluetoothOppBtErrorActivity.class);
289             in.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
290             in.putExtra("title", context.getString(R.string.unknown_file));
291             in.putExtra("content", context.getString(R.string.unknown_file_desc));
292             context.startActivity(in);
293         }
294     }
295 
fileExists(Context context, Uri uri)296     static boolean fileExists(Context context, Uri uri) {
297         // Open a specific media item using ParcelFileDescriptor.
298         ContentResolver resolver = context.getContentResolver();
299         String readOnlyMode = "r";
300         try (ParcelFileDescriptor unusedPfd =
301                 BluetoothMethodProxy.getInstance()
302                         .contentResolverOpenFileDescriptor(resolver, uri, readOnlyMode)) {
303             return true;
304         } catch (IOException e) {
305             ContentProfileErrorReportUtils.report(
306                     BluetoothProfile.OPP,
307                     BluetoothProtoEnums.BLUETOOTH_OPP_UTILITY,
308                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
309                     4);
310             Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
311         }
312         return false;
313     }
314 
315     /** To judge if the file type supported (can be handled by some app) by phone system. */
isRecognizedFileType(Context context, Uri fileUri, String mimetype)316     public static boolean isRecognizedFileType(Context context, Uri fileUri, String mimetype) {
317         boolean ret = true;
318 
319         Log.d(TAG, "RecognizedFileType() fileUri: " + fileUri + " mimetype: " + mimetype);
320 
321         Intent mimetypeIntent = new Intent(Intent.ACTION_VIEW);
322         mimetypeIntent.setDataAndType(
323                 fileUri.normalizeScheme(), Intent.normalizeMimeType(mimetype));
324         List<ResolveInfo> list =
325                 context.getPackageManager()
326                         .queryIntentActivities(mimetypeIntent, PackageManager.MATCH_DEFAULT_ONLY);
327 
328         if (list.size() == 0) {
329             Log.d(TAG, "NO application to handle MIME type " + mimetype);
330             ret = false;
331         }
332         return ret;
333     }
334 
335     /** update visibility to Hidden */
updateVisibilityToHidden(Context context, Uri uri)336     public static void updateVisibilityToHidden(Context context, Uri uri) {
337         ContentValues updateValues = new ContentValues();
338         updateValues.put(BluetoothShare.VISIBILITY, BluetoothShare.VISIBILITY_HIDDEN);
339         BluetoothMethodProxy.getInstance()
340                 .contentResolverUpdate(context.getContentResolver(), uri, updateValues, null, null);
341     }
342 
343     /** Helper function to build the progress text. */
formatProgressText(long totalBytes, long currentBytes)344     public static String formatProgressText(long totalBytes, long currentBytes) {
345         DecimalFormat df = new DecimalFormat("0%");
346         df.setRoundingMode(RoundingMode.DOWN);
347         double percent = 0.0;
348         if (totalBytes > 0) {
349             percent = currentBytes / (double) totalBytes;
350         }
351         return df.format(percent);
352     }
353 
354     /** Helper function to build the result notification text content. */
formatResultText(int countSuccess, int countUnsuccessful, Context context)355     static String formatResultText(int countSuccess, int countUnsuccessful, Context context) {
356         if (context == null) {
357             return null;
358         }
359         Map<String, Object> mapUnsuccessful = new HashMap<>();
360         mapUnsuccessful.put("count", countUnsuccessful);
361 
362         Map<String, Object> mapSuccess = new HashMap<>();
363         mapSuccess.put("count", countSuccess);
364 
365         return new MessageFormat(
366                         context.getResources()
367                                 .getString(
368                                         R.string.noti_caption_success,
369                                         new MessageFormat(
370                                                         context.getResources()
371                                                                 .getString(
372                                                                         R.string
373                                                                                 .noti_caption_unsuccessful),
374                                                         Locale.getDefault())
375                                                 .format(mapUnsuccessful)),
376                         Locale.getDefault())
377                 .format(mapSuccess);
378     }
379 
380     /** Whether the device has the "nosdcard" characteristic or not. */
deviceHasNoSdCard()381     public static boolean deviceHasNoSdCard() {
382         if (sNoSdCard == null) {
383             String characteristics = SystemProperties.get("ro.build.characteristics", "");
384             sNoSdCard = Arrays.asList(characteristics).contains("nosdcard");
385         }
386         return sNoSdCard;
387     }
388 
389     /** Get status description according to status code. */
getStatusDescription(Context context, int statusCode, String deviceName)390     public static String getStatusDescription(Context context, int statusCode, String deviceName) {
391         String ret;
392         if (statusCode == BluetoothShare.STATUS_PENDING) {
393             ret = context.getString(R.string.status_pending);
394         } else if (statusCode == BluetoothShare.STATUS_RUNNING) {
395             ret = context.getString(R.string.status_running);
396         } else if (statusCode == BluetoothShare.STATUS_SUCCESS) {
397             ret = context.getString(R.string.status_success);
398         } else if (statusCode == BluetoothShare.STATUS_NOT_ACCEPTABLE) {
399             ret = context.getString(R.string.status_not_accept);
400         } else if (statusCode == BluetoothShare.STATUS_FORBIDDEN) {
401             ret = context.getString(R.string.status_forbidden);
402         } else if (statusCode == BluetoothShare.STATUS_CANCELED) {
403             ret = context.getString(R.string.status_canceled);
404         } else if (statusCode == BluetoothShare.STATUS_FILE_ERROR) {
405             ret = context.getString(R.string.status_file_error);
406         } else if (statusCode == BluetoothShare.STATUS_ERROR_NO_SDCARD) {
407             int id =
408                     deviceHasNoSdCard()
409                             ? R.string.status_no_sd_card_nosdcard
410                             : R.string.status_no_sd_card_default;
411             ret = context.getString(id);
412         } else if (statusCode == BluetoothShare.STATUS_CONNECTION_ERROR) {
413             ret = context.getString(R.string.status_connection_error);
414         } else if (statusCode == BluetoothShare.STATUS_ERROR_SDCARD_FULL) {
415             int id = deviceHasNoSdCard() ? R.string.bt_sm_2_1_nosdcard : R.string.bt_sm_2_1_default;
416             ret = context.getString(id);
417         } else if ((statusCode == BluetoothShare.STATUS_BAD_REQUEST)
418                 || (statusCode == BluetoothShare.STATUS_LENGTH_REQUIRED)
419                 || (statusCode == BluetoothShare.STATUS_PRECONDITION_FAILED)
420                 || (statusCode == BluetoothShare.STATUS_UNHANDLED_OBEX_CODE)
421                 || (statusCode == BluetoothShare.STATUS_OBEX_DATA_ERROR)) {
422             ret = context.getString(R.string.status_protocol_error);
423         } else {
424             ret = context.getString(R.string.status_unknown_error);
425         }
426         return ret;
427     }
428 
429     /** Retry the failed transfer: Will insert a new transfer session to db */
retryTransfer(Context context, BluetoothOppTransferInfo transInfo)430     public static void retryTransfer(Context context, BluetoothOppTransferInfo transInfo) {
431         ContentValues values = new ContentValues();
432         values.put(BluetoothShare.URI, transInfo.mFileUri);
433         values.put(BluetoothShare.MIMETYPE, transInfo.mFileType);
434         values.put(BluetoothShare.DESTINATION, transInfo.mDestAddr);
435 
436         final Uri contentUri =
437                 context.getContentResolver().insert(BluetoothShare.CONTENT_URI, values);
438         Log.v(TAG, "Insert contentUri: " + contentUri + "  to device: " + transInfo.mDeviceName);
439     }
440 
originalUri(Uri uri)441     static Uri originalUri(Uri uri) {
442         String mUri = uri.toString();
443         int atIndex = mUri.lastIndexOf("@");
444         if (atIndex != -1) {
445             mUri = mUri.substring(0, atIndex);
446             uri = Uri.parse(mUri);
447         }
448         Log.v(TAG, "originalUri: " + uri);
449         return uri;
450     }
451 
generateUri(Uri uri, BluetoothOppSendFileInfo sendFileInfo)452     static Uri generateUri(Uri uri, BluetoothOppSendFileInfo sendFileInfo) {
453         String fileInfo = sendFileInfo.toString();
454         int atIndex = fileInfo.lastIndexOf("@");
455         fileInfo = fileInfo.substring(atIndex);
456         uri = Uri.parse(uri + fileInfo);
457         Log.v(TAG, "generateUri: " + uri);
458         return uri;
459     }
460 
putSendFileInfo(Uri uri, BluetoothOppSendFileInfo sendFileInfo)461     static void putSendFileInfo(Uri uri, BluetoothOppSendFileInfo sendFileInfo) {
462         Log.d(TAG, "putSendFileInfo: uri=" + uri + " sendFileInfo=" + sendFileInfo);
463         if (sendFileInfo == BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR) {
464             Log.e(TAG, "putSendFileInfo: bad sendFileInfo, URI: " + uri);
465             ContentProfileErrorReportUtils.report(
466                     BluetoothProfile.OPP,
467                     BluetoothProtoEnums.BLUETOOTH_OPP_UTILITY,
468                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
469                     5);
470         }
471         sSendFileMap.put(uri, sendFileInfo);
472     }
473 
getSendFileInfo(Uri uri)474     static BluetoothOppSendFileInfo getSendFileInfo(Uri uri) {
475         Log.d(TAG, "getSendFileInfo: uri=" + uri);
476         BluetoothOppSendFileInfo info = sSendFileMap.get(uri);
477         return (info != null) ? info : BluetoothOppSendFileInfo.SEND_FILE_INFO_ERROR;
478     }
479 
closeSendFileInfo(Uri uri)480     static void closeSendFileInfo(Uri uri) {
481         Log.d(TAG, "closeSendFileInfo: uri=" + uri);
482         BluetoothOppSendFileInfo info = sSendFileMap.remove(uri);
483         if (info != null && info.mInputStream != null) {
484             try {
485                 info.mInputStream.close();
486             } catch (IOException ignored) {
487                 ContentProfileErrorReportUtils.report(
488                         BluetoothProfile.OPP,
489                         BluetoothProtoEnums.BLUETOOTH_OPP_UTILITY,
490                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
491                         6);
492             }
493         }
494     }
495 
496     /**
497      * Checks if the URI is in Environment.getExternalStorageDirectory() as it is the only directory
498      * that is possibly readable by both the sender and the Bluetooth process.
499      */
isInExternalStorageDir(Uri uri)500     static boolean isInExternalStorageDir(Uri uri) {
501         if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
502             Log.e(TAG, "Not a file URI: " + uri);
503             ContentProfileErrorReportUtils.report(
504                     BluetoothProfile.OPP,
505                     BluetoothProtoEnums.BLUETOOTH_OPP_UTILITY,
506                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR,
507                     7);
508             return false;
509         }
510 
511         if ("file".equals(uri.getScheme())) {
512             String canonicalPath;
513             try {
514                 canonicalPath = new File(uri.getPath()).getCanonicalPath();
515             } catch (IOException e) {
516                 ContentProfileErrorReportUtils.report(
517                         BluetoothProfile.OPP,
518                         BluetoothProtoEnums.BLUETOOTH_OPP_UTILITY,
519                         BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
520                         8);
521                 canonicalPath = uri.getPath();
522             }
523             File file = new File(canonicalPath);
524             // if emulated
525             if (Environment.isExternalStorageEmulated()) {
526                 // Gets legacy external storage path
527                 final String legacyPath = new File(System.getenv("EXTERNAL_STORAGE")).toString();
528                 // Splice in user-specific path when legacy path is found
529                 if (canonicalPath.startsWith(legacyPath)) {
530                     file =
531                             new File(
532                                     Environment.getExternalStorageDirectory().toString(),
533                                     canonicalPath.substring(legacyPath.length() + 1));
534                 }
535             }
536             return isSameOrSubDirectory(Environment.getExternalStorageDirectory(), file);
537         }
538         return isSameOrSubDirectory(
539                 Environment.getExternalStorageDirectory(), new File(uri.getPath()));
540     }
541 
isForbiddenContent(Uri uri)542     static boolean isForbiddenContent(Uri uri) {
543         if ("com.android.bluetooth.map.MmsFileProvider".equals(uri.getHost())) {
544             return true;
545         }
546         return false;
547     }
548 
549     /**
550      * Checks, whether the child directory is the same as, or a sub-directory of the base directory.
551      * Neither base nor child should be null.
552      */
isSameOrSubDirectory(File base, File child)553     static boolean isSameOrSubDirectory(File base, File child) {
554         try {
555             base = base.getCanonicalFile();
556             child = child.getCanonicalFile();
557             File parentFile = child;
558             while (parentFile != null) {
559                 if (base.equals(parentFile)) {
560                     return true;
561                 }
562                 parentFile = parentFile.getParentFile();
563             }
564             return false;
565         } catch (IOException ex) {
566             ContentProfileErrorReportUtils.report(
567                     BluetoothProfile.OPP,
568                     BluetoothProtoEnums.BLUETOOTH_OPP_UTILITY,
569                     BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__EXCEPTION,
570                     9);
571             Log.e(TAG, "Error while accessing file", ex);
572             return false;
573         }
574     }
575 
cancelNotification(Context ctx)576     protected static void cancelNotification(Context ctx) {
577         NotificationManager nm = ctx.getSystemService(NotificationManager.class);
578         nm.cancel(BluetoothOppNotification.NOTIFICATION_ID_PROGRESS);
579     }
580 
581     /** Grants uri read permission to nearby sharing component. */
grantPermissionToNearbyComponent(Context context, List<Uri> uris)582     static void grantPermissionToNearbyComponent(Context context, List<Uri> uris) {
583         String packageName = getNearbyComponentPackageName(context);
584         if (packageName == null) {
585             return;
586         }
587         for (Uri uri : uris) {
588             BluetoothMethodProxy.getInstance()
589                     .grantUriPermission(
590                             context, packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
591         }
592     }
593 
getNearbyComponentPackageName(Context context)594     private static String getNearbyComponentPackageName(Context context) {
595         String componentString =
596                 Settings.Secure.getString(context.getContentResolver(), NEARBY_SHARING_COMPONENT);
597         if (componentString == null) {
598             return null;
599         }
600         ComponentName componentName = ComponentName.unflattenFromString(componentString);
601         if (componentName == null) {
602             return null;
603         }
604         return componentName.getPackageName();
605     }
606 }
607