Merge changes from topics 'List parent profile custom ringtones from managed profile', 'Add/delete custom ringtones from storage'
* changes: [RingtoneManager] Option for cursor to list parent sounds [RingtoneManager] API to delete custom ringtones [RingtoneManager] API to add custom ringtones
This commit is contained in:
committed by
Android (Google) Code Review
commit
677e297537
46
media/java/android/media/ExternalRingtonesCursorWrapper.java
Normal file
46
media/java/android/media/ExternalRingtonesCursorWrapper.java
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.media;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.database.Cursor;
|
||||
import android.database.CursorWrapper;
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* Cursor that adds the user id to fetched URIs. This is especially useful for {@link getCursor} as
|
||||
* a managed profile should also list its parent's ringtones
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class ExternalRingtonesCursorWrapper extends CursorWrapper {
|
||||
|
||||
private int mUserId;
|
||||
|
||||
public ExternalRingtonesCursorWrapper(Cursor cursor, int userId) {
|
||||
super(cursor);
|
||||
mUserId = userId;
|
||||
}
|
||||
|
||||
public String getString(int index) {
|
||||
String result = super.getString(index);
|
||||
if (index == RingtoneManager.URI_COLUMN_INDEX) {
|
||||
result = ContentProvider.maybeAddUserId(Uri.parse(result), mUserId).toString();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,11 @@
|
||||
package android.media;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.RequiresPermission;
|
||||
import android.annotation.SdkConstant;
|
||||
import android.annotation.SdkConstant.SdkConstantType;
|
||||
import android.annotation.WorkerThread;
|
||||
import android.app.Activity;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentResolver;
|
||||
@@ -28,6 +30,7 @@ import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.UserInfo;
|
||||
import android.database.Cursor;
|
||||
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
@@ -43,11 +46,16 @@ import com.android.internal.database.SortCursor;
|
||||
|
||||
import libcore.io.Streams;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import static android.content.ContentProvider.maybeAddUserId;
|
||||
import static android.content.pm.PackageManager.NameNotFoundException;
|
||||
@@ -253,6 +261,8 @@ public class RingtoneManager {
|
||||
private boolean mStopPreviousRingtone = true;
|
||||
private Ringtone mPreviousRingtone;
|
||||
|
||||
private boolean mIncludeParentRingtones;
|
||||
|
||||
/**
|
||||
* Constructs a RingtoneManager. This constructor is recommended as its
|
||||
* constructed instance manages cursor(s).
|
||||
@@ -260,9 +270,24 @@ public class RingtoneManager {
|
||||
* @param activity The activity used to get a managed cursor.
|
||||
*/
|
||||
public RingtoneManager(Activity activity) {
|
||||
this(activity, /* includeParentRingtones */ false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a RingtoneManager. This constructor is recommended if there's the need to also
|
||||
* list ringtones from the user's parent.
|
||||
*
|
||||
* @param activity The activity used to get a managed cursor.
|
||||
* @param includeParentRingtones if true, this ringtone manager's cursor will also retrieve
|
||||
* ringtones from the parent of the user specified in the given activity
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public RingtoneManager(Activity activity, boolean includeParentRingtones) {
|
||||
mActivity = activity;
|
||||
mContext = activity;
|
||||
setType(mType);
|
||||
mIncludeParentRingtones = includeParentRingtones;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,9 +298,23 @@ public class RingtoneManager {
|
||||
* @param context The context to used to get a cursor.
|
||||
*/
|
||||
public RingtoneManager(Context context) {
|
||||
this(context, /* includeParentRingtones */ false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a RingtoneManager.
|
||||
*
|
||||
* @param context The context to used to get a cursor.
|
||||
* @param includeParentRingtones if true, this ringtone manager's cursor will also retrieve
|
||||
* ringtones from the parent of the user specified in the given context
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public RingtoneManager(Context context, boolean includeParentRingtones) {
|
||||
mActivity = null;
|
||||
mContext = context;
|
||||
setType(mType);
|
||||
mIncludeParentRingtones = includeParentRingtones;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -395,14 +434,38 @@ public class RingtoneManager {
|
||||
if (mCursor != null && mCursor.requery()) {
|
||||
return mCursor;
|
||||
}
|
||||
|
||||
final Cursor internalCursor = getInternalRingtones();
|
||||
final Cursor mediaCursor = getMediaRingtones();
|
||||
|
||||
return mCursor = new SortCursor(new Cursor[] { internalCursor, mediaCursor },
|
||||
|
||||
ArrayList<Cursor> ringtoneCursors = new ArrayList<Cursor>();
|
||||
ringtoneCursors.add(getInternalRingtones());
|
||||
ringtoneCursors.add(getMediaRingtones());
|
||||
|
||||
if (mIncludeParentRingtones) {
|
||||
Cursor parentRingtonesCursor = getParentProfileRingtones();
|
||||
if (parentRingtonesCursor != null) {
|
||||
ringtoneCursors.add(parentRingtonesCursor);
|
||||
}
|
||||
}
|
||||
|
||||
return mCursor = new SortCursor(ringtoneCursors.toArray(new Cursor[ringtoneCursors.size()]),
|
||||
MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
|
||||
}
|
||||
|
||||
private Cursor getParentProfileRingtones() {
|
||||
final UserManager um = UserManager.get(mContext);
|
||||
final UserInfo parentInfo = um.getProfileParent(mContext.getUserId());
|
||||
if (parentInfo != null && parentInfo.id != mContext.getUserId()) {
|
||||
final Context parentContext = createPackageContextAsUser(mContext, parentInfo.id);
|
||||
if (parentContext != null) {
|
||||
// We don't need to re-add the internal ringtones for the work profile since
|
||||
// they are the same as the personal profile. We just need the external
|
||||
// ringtones.
|
||||
return new ExternalRingtonesCursorWrapper(getMediaRingtones(parentContext),
|
||||
parentInfo.id);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a {@link Ringtone} for the ringtone at the given position in the
|
||||
* {@link Cursor}.
|
||||
@@ -434,7 +497,35 @@ public class RingtoneManager {
|
||||
|
||||
return getUriFromCursor(mCursor);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Queries the database for the Uri to a ringtone in a specific path (the ringtone has to have
|
||||
* been scanned before)
|
||||
*
|
||||
* @param context Context used to query the database
|
||||
* @param path Path to the ringtone file
|
||||
* @return Uri of the ringtone, null if something fails in the query or the ringtone doesn't
|
||||
* exist
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
private static Uri getExistingRingtoneUriFromPath(Context context, String path) {
|
||||
final String[] proj = {MediaStore.Audio.Media._ID};
|
||||
final String[] selectionArgs = {path};
|
||||
try (final Cursor cursor = context.getContentResolver().query(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, proj,
|
||||
MediaStore.Audio.Media.DATA + "=? ", selectionArgs, /* sortOrder */ null)) {
|
||||
if (cursor == null || !cursor.moveToFirst()) {
|
||||
return null;
|
||||
}
|
||||
final int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
|
||||
if (id == -1) {
|
||||
return null;
|
||||
}
|
||||
return Uri.withAppendedPath(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, "" + id);
|
||||
}
|
||||
}
|
||||
|
||||
private static Uri getUriFromCursor(Cursor cursor) {
|
||||
return ContentUris.withAppendedId(Uri.parse(cursor.getString(URI_COLUMN_INDEX)), cursor
|
||||
.getLong(ID_COLUMN_INDEX));
|
||||
@@ -523,7 +614,11 @@ public class RingtoneManager {
|
||||
}
|
||||
|
||||
private Cursor getMediaRingtones() {
|
||||
if (PackageManager.PERMISSION_GRANTED != mContext.checkPermission(
|
||||
return getMediaRingtones(mContext);
|
||||
}
|
||||
|
||||
private Cursor getMediaRingtones(Context context) {
|
||||
if (PackageManager.PERMISSION_GRANTED != context.checkPermission(
|
||||
android.Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Process.myPid(), Process.myUid())) {
|
||||
Log.w(TAG, "No READ_EXTERNAL_STORAGE permission, ignoring ringtones on ext storage");
|
||||
@@ -537,7 +632,7 @@ public class RingtoneManager {
|
||||
? query(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MEDIA_COLUMNS,
|
||||
constructBooleanTrueWhereClause(mFilterColumns), null,
|
||||
MediaStore.Audio.Media.DEFAULT_SORT_ORDER)
|
||||
MediaStore.Audio.Media.DEFAULT_SORT_ORDER, context)
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -592,10 +687,19 @@ public class RingtoneManager {
|
||||
String selection,
|
||||
String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
return query(uri, projection, selection, selectionArgs, sortOrder, mContext);
|
||||
}
|
||||
|
||||
private Cursor query(Uri uri,
|
||||
String[] projection,
|
||||
String selection,
|
||||
String[] selectionArgs,
|
||||
String sortOrder,
|
||||
Context context) {
|
||||
if (mActivity != null) {
|
||||
return mActivity.managedQuery(uri, projection, selection, selectionArgs, sortOrder);
|
||||
} else {
|
||||
return mContext.getContentResolver().query(uri, projection, selection, selectionArgs,
|
||||
return context.getContentResolver().query(uri, projection, selection, selectionArgs,
|
||||
sortOrder);
|
||||
}
|
||||
}
|
||||
@@ -642,7 +746,29 @@ public class RingtoneManager {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Look up the path for a given {@link Uri} referring to a ringtone sound (TYPE_RINGTONE,
|
||||
* TYPE_NOTIFICATION, or TYPE_ALARM). This is saved in {@link MediaStore.Audio.Media#DATA}.
|
||||
*
|
||||
* @return a {@link File} pointing at the location of the {@param uri} on disk, or {@code null}
|
||||
* if there is no such file.
|
||||
*/
|
||||
private File getRingtonePathFromUri(Uri uri) {
|
||||
// Query cursor to get ringtone path
|
||||
final String[] projection = {MediaStore.Audio.Media.DATA};
|
||||
setFilterColumnsList(TYPE_RINGTONE | TYPE_NOTIFICATION | TYPE_ALARM);
|
||||
|
||||
String path = null;
|
||||
try (Cursor cursor = query(uri, projection, constructBooleanTrueWhereClause(mFilterColumns),
|
||||
null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
path = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA));
|
||||
}
|
||||
}
|
||||
return path != null ? new File(path) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables Settings.System.SYNC_PARENT_SOUNDS, copying the parent's ringtones to the current
|
||||
* profile
|
||||
@@ -659,17 +785,14 @@ public class RingtoneManager {
|
||||
UserManager um = UserManager.get(userContext);
|
||||
UserInfo parentInfo = um.getProfileParent(userContext.getUserId());
|
||||
if (parentInfo != null) {
|
||||
try {
|
||||
Context targetContext = userContext.createPackageContextAsUser(
|
||||
userContext.getPackageName(), 0 /* flags */, UserHandle.of(parentInfo.id));
|
||||
final Context targetContext = createPackageContextAsUser(userContext, parentInfo.id);
|
||||
if (targetContext != null) {
|
||||
for (int ringtoneType : RINGTONE_TYPES) {
|
||||
Uri ringtoneUri = getActualDefaultRingtoneUri(targetContext, ringtoneType);
|
||||
// Add user id of parent so that custom ringtones can be read and played
|
||||
RingtoneManager.setActualDefaultRingtoneUri(userContext, ringtoneType,
|
||||
maybeAddUserId(ringtoneUri, parentInfo.id));
|
||||
}
|
||||
} catch (NameNotFoundException e) {
|
||||
Log.e(TAG, "Unable to create parent context", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -749,9 +872,157 @@ public class RingtoneManager {
|
||||
}
|
||||
|
||||
private static boolean isInternalRingtoneUri(Uri uri) {
|
||||
Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(uri);
|
||||
return uriWithoutUserId == null ? false : uriWithoutUserId.toString()
|
||||
.startsWith(MediaStore.Audio.Media.INTERNAL_CONTENT_URI.toString());
|
||||
return isRingtoneUriInStorage(uri, MediaStore.Audio.Media.INTERNAL_CONTENT_URI);
|
||||
}
|
||||
|
||||
private static boolean isExternalRingtoneUri(Uri uri) {
|
||||
return isRingtoneUriInStorage(uri, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
|
||||
}
|
||||
|
||||
private static boolean isRingtoneUriInStorage(Uri ringtone, Uri storage) {
|
||||
Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(ringtone);
|
||||
return uriWithoutUserId == null ? false
|
||||
: uriWithoutUserId.toString().startsWith(storage.toString());
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public boolean isCustomRingtone(Uri uri) {
|
||||
if(!isExternalRingtoneUri(uri)) {
|
||||
// A custom ringtone would be in the external storage
|
||||
return false;
|
||||
}
|
||||
|
||||
final File ringtoneFile = (uri == null ? null : getRingtonePathFromUri(uri));
|
||||
final File parent = (ringtoneFile == null ? null : ringtoneFile.getParentFile());
|
||||
if (parent == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final String[] directories = {
|
||||
Environment.DIRECTORY_RINGTONES,
|
||||
Environment.DIRECTORY_NOTIFICATIONS,
|
||||
Environment.DIRECTORY_ALARMS
|
||||
};
|
||||
for (final String directory : directories) {
|
||||
if (parent.equals(Environment.getExternalStoragePublicDirectory(directory))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an audio file to the list of ringtones.
|
||||
*
|
||||
* After making sure the given file is an audio file, copies the file to the ringtone storage,
|
||||
* and asks the {@link android.media.MediaScanner} to scan that file. This call will block until
|
||||
* the scan is completed.
|
||||
*
|
||||
* The directory where the copied file is stored is the directory that matches the ringtone's
|
||||
* type, which is one of: {@link android.is.Environment#DIRECTORY_RINGTONES};
|
||||
* {@link android.is.Environment#DIRECTORY_NOTIFICATIONS};
|
||||
* {@link android.is.Environment#DIRECTORY_ALARMS}.
|
||||
*
|
||||
* This does not allow modifying the type of an existing ringtone file. To change type, use the
|
||||
* APIs in {@link android.content.ContentResolver} to update the corresponding columns.
|
||||
*
|
||||
* @param fileUri Uri of the file to be added as ringtone. Must be a media file.
|
||||
* @param type The type of the ringtone to be added. Must be one of {@link #TYPE_RINGTONE},
|
||||
* {@link #TYPE_NOTIFICATION}, or {@link #TYPE_ALARM}.
|
||||
*
|
||||
* @return The Uri of the installed ringtone, which may be the Uri of {@param fileUri} if it is
|
||||
* already in ringtone storage.
|
||||
*
|
||||
* @throws FileNotFoundexception if an appropriate unique filename to save the new ringtone file
|
||||
* as cannot be found, for example if the unique name is too long.
|
||||
* @throws IllegalArgumentException if {@param fileUri} does not point to an existing audio
|
||||
* file, or if the {@param type} is not one of the accepted ringtone types.
|
||||
* @throws IOException if the audio file failed to copy to ringtone storage; for example, if
|
||||
* external storage was not available, or if the file was copied but the media scanner
|
||||
* did not recognize it as a ringtone.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@WorkerThread
|
||||
public Uri addCustomExternalRingtone(@NonNull final Uri fileUri, final int type)
|
||||
throws FileNotFoundException, IllegalArgumentException, IOException {
|
||||
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||
throw new IOException("External storage is not mounted. Unable to install ringtones.");
|
||||
}
|
||||
|
||||
// Sanity-check: are we actually being asked to install an audio file?
|
||||
final String mimeType = mContext.getContentResolver().getType(fileUri);
|
||||
if(mimeType == null || !mimeType.startsWith("audio/")) {
|
||||
throw new IllegalArgumentException("Ringtone file must have MIME type \"audio/*\"."
|
||||
+ " Given file has MIME type \"" + mimeType + "\"");
|
||||
}
|
||||
|
||||
// Choose a directory to save the ringtone. Only one type of installation at a time is
|
||||
// allowed. Throws IllegalArgumentException if anything else is given.
|
||||
final String subdirectory = getExternalDirectoryForType(type);
|
||||
|
||||
// Find a filename. Throws FileNotFoundException if none can be found.
|
||||
final File outFile = Utils.getUniqueExternalFile(mContext, subdirectory,
|
||||
Utils.getFileDisplayNameFromUri(mContext, fileUri), mimeType);
|
||||
|
||||
// Copy contents to external ringtone storage. Throws IOException if the copy fails.
|
||||
try (final InputStream input = mContext.getContentResolver().openInputStream(fileUri);
|
||||
final OutputStream output = new FileOutputStream(outFile)) {
|
||||
Streams.copy(input, output);
|
||||
}
|
||||
|
||||
// Tell MediaScanner about the new file. Wait for it to assign a {@link Uri}.
|
||||
try (NewRingtoneScanner scanner = new NewRingtoneScanner(outFile)) {
|
||||
return scanner.take();
|
||||
} catch (InterruptedException e) {
|
||||
throw new IOException("Audio file failed to scan as a ringtone", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String getExternalDirectoryForType(final int type) {
|
||||
switch (type) {
|
||||
case TYPE_RINGTONE:
|
||||
return Environment.DIRECTORY_RINGTONES;
|
||||
case TYPE_NOTIFICATION:
|
||||
return Environment.DIRECTORY_NOTIFICATIONS;
|
||||
case TYPE_ALARM:
|
||||
return Environment.DIRECTORY_ALARMS;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported ringtone type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the actual file in the Uri and its ringtone database entry if the Uri's actual path
|
||||
* is in one of the following directories: {@link android.is.Environment#DIRECTORY_RINGTONES},
|
||||
* {@link android.is.Environment#DIRECTORY_NOTIFICATIONS} or
|
||||
* {@link android.is.Environment#DIRECTORY_ALARMS}.
|
||||
*
|
||||
* The given Uri must be a ringtone Content Uri.
|
||||
*
|
||||
* Keep in mind that if the ringtone deleted is a default ringtone, it will still live in the
|
||||
* ringtone cache file so it will be playable from there. However, if an app uses the ringtone
|
||||
* as its own ringtone, it won't be played, which is the same behavior observed for 3rd party
|
||||
* custom ringtones.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public boolean deleteExternalRingtone(Uri uri) {
|
||||
if(!isCustomRingtone(uri)) {
|
||||
// We can only delete custom ringtones in the default ringtone storages
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save the path of the ringtone before deleting from our content resolver.
|
||||
final File ringtoneFile = getRingtonePathFromUri(uri);
|
||||
try {
|
||||
if (ringtoneFile != null && mContext.getContentResolver().delete(uri, null, null) > 0) {
|
||||
return ringtoneFile.delete();
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
Log.d(TAG, "Unable to delete custom ringtone", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -860,5 +1131,67 @@ public class RingtoneManager {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a {@link android.media.MediaScannerConnection} to scan a ringtone file and add its
|
||||
* information to the internal database.
|
||||
*
|
||||
* It uses a {@link java.util.concurrent.LinkedBlockingQueue} so that the caller can block until
|
||||
* the scan is completed.
|
||||
*/
|
||||
private class NewRingtoneScanner implements Closeable, MediaScannerConnectionClient {
|
||||
private MediaScannerConnection mMediaScannerConnection;
|
||||
private File mFile;
|
||||
private LinkedBlockingQueue<Uri> mQueue = new LinkedBlockingQueue<>(1);
|
||||
|
||||
public NewRingtoneScanner(File file) {
|
||||
mFile = file;
|
||||
mMediaScannerConnection = new MediaScannerConnection(mContext, this);
|
||||
mMediaScannerConnection.connect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
mMediaScannerConnection.disconnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaScannerConnected() {
|
||||
mMediaScannerConnection.scanFile(mFile.getAbsolutePath(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanCompleted(String path, Uri uri) {
|
||||
if (uri == null) {
|
||||
// There was some issue with scanning. Delete the copied file so it is not oprhaned.
|
||||
mFile.delete();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mQueue.put(uri);
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Unable to put new ringtone Uri in queue", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Uri take() throws InterruptedException {
|
||||
return mQueue.take();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to create a context for the given user.
|
||||
*
|
||||
* @return created context, or null if package does not exist
|
||||
* @hide
|
||||
*/
|
||||
private static Context createPackageContextAsUser(Context context, int userId) {
|
||||
try {
|
||||
return context.createPackageContextAsUser(context.getPackageName(), 0 /* flags */,
|
||||
UserHandle.of(userId));
|
||||
} catch (NameNotFoundException e) {
|
||||
Log.e(TAG, "Unable to create package context", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,21 @@
|
||||
|
||||
package android.media;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.ContentResolver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.os.FileUtils;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.util.Range;
|
||||
import android.util.Rational;
|
||||
import android.util.Size;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Vector;
|
||||
@@ -307,4 +316,66 @@ class Utils {
|
||||
Log.w(TAG, "could not parse size range '" + o + "'");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unique file in the specified external storage with the desired name. If the name is
|
||||
* taken, the new file's name will have '(%d)' to avoid overwriting files.
|
||||
*
|
||||
* @param context {@link Context} to query the file name from.
|
||||
* @param subdirectory One of the directories specified in {@link android.os.Environment}
|
||||
* @param fileName desired name for the file.
|
||||
* @param mimeType MIME type of the file to create.
|
||||
* @return the File object in the storage, or null if an error occurs.
|
||||
*/
|
||||
public static File getUniqueExternalFile(Context context, String subdirectory, String fileName,
|
||||
String mimeType) {
|
||||
File externalStorage = Environment.getExternalStoragePublicDirectory(subdirectory);
|
||||
// Make sure the storage subdirectory exists
|
||||
externalStorage.mkdirs();
|
||||
|
||||
File outFile = null;
|
||||
try {
|
||||
// Ensure the file has a unique name, as to not override any existing file
|
||||
outFile = FileUtils.buildUniqueFile(externalStorage, mimeType, fileName);
|
||||
} catch (FileNotFoundException e) {
|
||||
// This might also be reached if the number of repeated files gets too high
|
||||
Log.e(TAG, "Unable to get a unique file name: " + e);
|
||||
return null;
|
||||
}
|
||||
return outFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a file's display name from its {@link android.content.ContentResolver.SCHEME_FILE}
|
||||
* or {@link android.content.ContentResolver.SCHEME_CONTENT} Uri. The display name of a file
|
||||
* includes its extension.
|
||||
*
|
||||
* @param context Context trying to resolve the file's display name.
|
||||
* @param uri Uri of the file.
|
||||
* @return the file's display name, or the uri's string if something fails or the uri isn't in
|
||||
* the schemes specified above.
|
||||
*/
|
||||
static String getFileDisplayNameFromUri(Context context, Uri uri) {
|
||||
String scheme = uri.getScheme();
|
||||
|
||||
if (ContentResolver.SCHEME_FILE.equals(scheme)) {
|
||||
return uri.getLastPathSegment();
|
||||
} else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
|
||||
// We need to query the ContentResolver to get the actual file name as the Uri masks it.
|
||||
// This means we want the name used for display purposes only.
|
||||
String[] proj = {
|
||||
OpenableColumns.DISPLAY_NAME
|
||||
};
|
||||
try (Cursor cursor = context.getContentResolver().query(uri, proj, null, null, null)) {
|
||||
if (cursor != null && cursor.getCount() != 0) {
|
||||
cursor.moveToFirst();
|
||||
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This will only happen if the Uri isn't either SCHEME_CONTENT or SCHEME_FILE, so we assume
|
||||
// it already represents the file's name.
|
||||
return uri.toString();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user