Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit abc303c3 authored by Wenhao Shi's avatar Wenhao Shi Committed by Android (Google) Code Review
Browse files

Merge changes from topic "cherrypicker-L90400000962489226:N88700001395452092" into udc-qpr-dev

* changes:
  Fix custom ringtone restore in Notification channels.
  Implemement the media lookup for ringtone restore in SettingsProvider.
parents 40c22ffc ba4b5739
Loading
Loading
Loading
Loading
+30 −7
Original line number Diff line number Diff line
@@ -26,12 +26,14 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.media.AudioAttributes;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.Settings;
import android.service.notification.NotificationListenerService;
import android.text.TextUtils;
import android.util.Log;
import android.util.proto.ProtoOutputStream;

import com.android.internal.util.Preconditions;
@@ -54,6 +56,7 @@ import java.util.Objects;
 * A representation of settings that apply to a collection of similarly themed notifications.
 */
public final class NotificationChannel implements Parcelable {
    private static final String TAG = "NotificationChannel";

    /**
     * The id of the default channel for an app. This id is reserved by the system. All
@@ -959,8 +962,11 @@ public final class NotificationChannel implements Parcelable {
        setLockscreenVisibility(safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY));

        Uri sound = safeUri(parser, ATT_SOUND);
        setSound(forRestore ? restoreSoundUri(context, sound, pkgInstalled) : sound,
                safeAudioAttributes(parser));

        final AudioAttributes audioAttributes = safeAudioAttributes(parser);
        final int usage = audioAttributes.getUsage();
        setSound(forRestore ? restoreSoundUri(context, sound, pkgInstalled, usage) : sound,
                audioAttributes);

        enableLights(safeBool(parser, ATT_LIGHTS, false));
        setLightColor(safeInt(parser, ATT_LIGHT_COLOR, DEFAULT_LIGHT_COLOR));
@@ -1010,18 +1016,34 @@ public final class NotificationChannel implements Parcelable {
        if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
            return uri;
        }

        return contentResolver.canonicalize(uri);
    }

    @Nullable
    private Uri getUncanonicalizedSoundUri(ContentResolver contentResolver, @NonNull Uri uri) {
    private Uri getUncanonicalizedSoundUri(
            ContentResolver contentResolver, @NonNull Uri uri, int usage) {
        if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(uri)
                || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())
                || ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
            return uri;
        }
        return contentResolver.uncanonicalize(uri);
        int ringtoneType = 0;

        // Consistent with UI(SoundPreferenceController.handlePreferenceTreeClick).
        if (AudioAttributes.USAGE_ALARM == usage) {
            ringtoneType = RingtoneManager.TYPE_ALARM;
        } else if (AudioAttributes.USAGE_NOTIFICATION_RINGTONE == usage) {
            ringtoneType = RingtoneManager.TYPE_RINGTONE;
        } else {
            ringtoneType = RingtoneManager.TYPE_NOTIFICATION;
        }
        try {
            return RingtoneManager.getRingtoneUriForRestore(
                    contentResolver, uri.toString(), ringtoneType);
        } catch (Exception e) {
            Log.e(TAG, "Failed to uncanonicalized sound uri for " + uri + " " + e);
            return Settings.System.DEFAULT_NOTIFICATION_URI;
        }
    }

    /**
@@ -1033,7 +1055,8 @@ public final class NotificationChannel implements Parcelable {
     * @hide
     */
    @Nullable
    public Uri restoreSoundUri(Context context, @Nullable Uri uri, boolean pkgInstalled) {
    public Uri restoreSoundUri(
            Context context, @Nullable Uri uri, boolean pkgInstalled, int usage) {
        if (uri == null || Uri.EMPTY.equals(uri)) {
            return null;
        }
@@ -1060,7 +1083,7 @@ public final class NotificationChannel implements Parcelable {
            }
        }
        mSoundRestored = true;
        return getUncanonicalizedSoundUri(contentResolver, canonicalizedUri);
        return getUncanonicalizedSoundUri(contentResolver, canonicalizedUri, usage);
    }

    /**
+250 −0
Original line number Diff line number Diff line
@@ -16,19 +16,52 @@

package android.app;

import static com.google.common.truth.Truth.assertThat;

import static junit.framework.TestCase.assertEquals;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.content.AttributionSource;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.IContentProvider;
import android.content.pm.ApplicationInfo;
import android.database.MatrixCursor;
import android.media.AudioAttributes;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcel;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.provider.MediaStore.Audio.AudioColumns;
import android.test.mock.MockContentResolver;
import android.util.Xml;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;

import com.google.common.base.Strings;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;

@RunWith(AndroidJUnit4.class)
@@ -36,6 +69,88 @@ import java.lang.reflect.Field;
public class NotificationChannelTest {
    private final String CLASS = "android.app.NotificationChannel";

    Context mContext;
    ContentProvider mContentProvider;
    IContentProvider mIContentProvider;

    @Before
    public void setUp() throws Exception {
        mContext = mock(Context.class);
        when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo());
        MockContentResolver mContentResolver = new MockContentResolver(mContext);
        when(mContext.getContentResolver()).thenReturn(mContentResolver);
        mContentProvider = mock(ContentProvider.class);
        mIContentProvider = mock(IContentProvider.class);
        when(mContentProvider.getIContentProvider()).thenReturn(mIContentProvider);
        doAnswer(
                invocation -> {
                        AttributionSource attributionSource = invocation.getArgument(0);
                        Uri uri = invocation.getArgument(1);
                        RemoteCallback cb = invocation.getArgument(2);
                        IContentProvider mock = (IContentProvider) (invocation.getMock());
                        AsyncTask.SERIAL_EXECUTOR.execute(
                                () -> {
                                final Bundle bundle = new Bundle();
                                try {
                                        bundle.putParcelable(
                                                ContentResolver.REMOTE_CALLBACK_RESULT,
                                                mock.canonicalize(attributionSource, uri));
                                } catch (RemoteException e) {
                                        /* consume */
                                }
                                cb.sendResult(bundle);
                                });
                        return null;
                })
            .when(mIContentProvider)
            .canonicalizeAsync(any(), any(), any());
        doAnswer(
                invocation -> {
                        AttributionSource attributionSource = invocation.getArgument(0);
                        Uri uri = invocation.getArgument(1);
                        RemoteCallback cb = invocation.getArgument(2);
                        IContentProvider mock = (IContentProvider) (invocation.getMock());
                        AsyncTask.SERIAL_EXECUTOR.execute(
                                () -> {
                                final Bundle bundle = new Bundle();
                                try {
                                        bundle.putParcelable(
                                                ContentResolver.REMOTE_CALLBACK_RESULT,
                                                mock.uncanonicalize(attributionSource, uri));
                                } catch (RemoteException e) {
                                        /* consume */
                                }
                                cb.sendResult(bundle);
                                });
                        return null;
                })
            .when(mIContentProvider)
            .uncanonicalizeAsync(any(), any(), any());
        doAnswer(
                invocation -> {
                        Uri uri = invocation.getArgument(0);
                        RemoteCallback cb = invocation.getArgument(1);
                        IContentProvider mock = (IContentProvider) (invocation.getMock());
                        AsyncTask.SERIAL_EXECUTOR.execute(
                                () -> {
                                final Bundle bundle = new Bundle();
                                try {
                                        bundle.putString(
                                                ContentResolver.REMOTE_CALLBACK_RESULT,
                                                mock.getType(uri));
                                } catch (RemoteException e) {
                                        /* consume */
                                }
                                cb.sendResult(bundle);
                                });
                        return null;
                })
            .when(mIContentProvider)
            .getTypeAsync(any(), any());

        mContentResolver.addProvider("media", mContentProvider);
    }

    @Test
    public void testLongStringFields() {
        NotificationChannel channel = new NotificationChannel("id", "name", 3);
@@ -103,4 +218,139 @@ public class NotificationChannelTest {
        assertEquals(NotificationChannel.MAX_TEXT_LENGTH,
                fromParcel.getSound().toString().length());
    }

    @Test
    public void testRestoreSoundUri_customLookup() throws Exception {
        Uri uriToBeRestoredUncanonicalized = Uri.parse("content://media/1");
        Uri uriToBeRestoredCanonicalized = Uri.parse("content://media/1?title=Song&canonical=1");
        Uri uriAfterRestoredUncanonicalized = Uri.parse("content://media/100");
        Uri uriAfterRestoredCanonicalized = Uri.parse("content://media/100?title=Song&canonical=1");

        NotificationChannel channel = new NotificationChannel("id", "name", 3);

        MatrixCursor cursor = new MatrixCursor(new String[] {"_id"});
        cursor.addRow(new Object[] {100L});

        when(mIContentProvider.canonicalize(any(), eq(uriToBeRestoredUncanonicalized)))
                .thenReturn(uriToBeRestoredCanonicalized);

        // Mock the failure of regular uncanonicalize.
        when(mIContentProvider.uncanonicalize(any(), eq(uriToBeRestoredCanonicalized)))
                .thenReturn(null);

        // Mock the custom lookup in RingtoneManager.getRingtoneUriForRestore.
        when(mIContentProvider.query(any(), any(), any(), any(), any())).thenReturn(cursor);

        // Mock the canonicalize in RingtoneManager.getRingtoneUriForRestore.
        when(mIContentProvider.canonicalize(any(), eq(uriAfterRestoredUncanonicalized)))
                .thenReturn(uriAfterRestoredCanonicalized);

        assertThat(
                        channel.restoreSoundUri(
                                mContext,
                                uriToBeRestoredUncanonicalized,
                                true,
                                AudioAttributes.USAGE_NOTIFICATION))
                .isEqualTo(uriAfterRestoredCanonicalized);
    }

    @Test
    public void testWriteXmlForBackup_customLookup_notificationUsage() throws Exception {
        testWriteXmlForBackup_customLookup(
                AudioAttributes.USAGE_NOTIFICATION, AudioColumns.IS_NOTIFICATION);
    }

    @Test
    public void testWriteXmlForBackup_customLookup_alarmUsage() throws Exception {
        testWriteXmlForBackup_customLookup(AudioAttributes.USAGE_ALARM, AudioColumns.IS_ALARM);
    }

    @Test
    public void testWriteXmlForBackup_customLookup_ringtoneUsage() throws Exception {
        testWriteXmlForBackup_customLookup(
                AudioAttributes.USAGE_NOTIFICATION_RINGTONE, AudioColumns.IS_RINGTONE);
    }

    @Test
    public void testWriteXmlForBackup_customLookup_unknownUsage() throws Exception {
        testWriteXmlForBackup_customLookup(
                AudioAttributes.USAGE_UNKNOWN, AudioColumns.IS_NOTIFICATION);
    }

    private void testWriteXmlForBackup_customLookup(int usage, String customQuerySelection)
            throws Exception {
        Uri uriToBeRestoredUncanonicalized = Uri.parse("content://media/1");
        Uri uriToBeRestoredCanonicalized = Uri.parse("content://media/1?title=Song&canonical=1");
        Uri uriAfterRestoredUncanonicalized = Uri.parse("content://media/100");
        Uri uriAfterRestoredCanonicalized = Uri.parse("content://media/100?title=Song&canonical=1");

        AudioAttributes mAudioAttributes =
                new AudioAttributes.Builder()
                        .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
                        .setUsage(usage)
                        .setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
                        .build();

        NotificationChannel channel = new NotificationChannel("id", "name", 3);
        channel.setSound(uriToBeRestoredCanonicalized, mAudioAttributes);

        TypedXmlSerializer serializer = Xml.newFastSerializer();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
        serializer.startDocument(null, true);

        // mock the canonicalize in writeXmlForBackup -> getSoundForBackup
        when(mIContentProvider.canonicalize(any(), eq(uriToBeRestoredUncanonicalized)))
                .thenReturn(uriToBeRestoredCanonicalized);
        when(mIContentProvider.canonicalize(any(), eq(uriToBeRestoredCanonicalized)))
                .thenReturn(uriToBeRestoredCanonicalized);

        channel.writeXmlForBackup(serializer, mContext);
        serializer.endDocument();
        serializer.flush();

        TypedXmlPullParser parser = Xml.newFastPullParser();
        byte[] byteArray = baos.toByteArray();
        parser.setInput(new BufferedInputStream(new ByteArrayInputStream(byteArray)), null);
        parser.nextTag();

        NotificationChannel targetChannel = new NotificationChannel("id", "name", 3);

        MatrixCursor cursor = new MatrixCursor(new String[] {"_id"});
        cursor.addRow(new Object[] {100L});

        when(mIContentProvider.canonicalize(any(), eq(uriToBeRestoredCanonicalized)))
                .thenReturn(uriToBeRestoredCanonicalized);

        // Mock the failure of regular uncanonicalize.
        when(mIContentProvider.uncanonicalize(any(), eq(uriToBeRestoredCanonicalized)))
                .thenReturn(null);

        Bundle expectedBundle =
                ContentResolver.createSqlQueryBundle(
                        customQuerySelection + "=1 AND title=?", new String[] {"Song"}, null);

        // Mock the custom lookup in RingtoneManager.getRingtoneUriForRestore.
        when(mIContentProvider.query(
                        any(),
                        any(),
                        any(),
                        // any(),
                        argThat(
                                queryBundle -> {
                                    return queryBundle != null
                                            && expectedBundle
                                                    .toString()
                                                    .equals(queryBundle.toString());
                                }),
                        any()))
                .thenReturn(cursor);

        // Mock the canonicalize in RingtoneManager.getRingtoneUriForRestore.
        when(mIContentProvider.canonicalize(any(), eq(uriAfterRestoredUncanonicalized)))
                .thenReturn(uriAfterRestoredCanonicalized);

        targetChannel.populateFromXmlForRestore(parser, true, mContext);
        assertThat(targetChannel.getSound()).isEqualTo(uriAfterRestoredCanonicalized);
    }
}
+91 −0
Original line number Diff line number Diff line
@@ -46,7 +46,9 @@ import android.os.ServiceManager;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AudioColumns;
import android.provider.MediaStore.MediaColumns;
import android.provider.Settings;
import android.provider.Settings.System;
@@ -507,6 +509,95 @@ public class RingtoneManager {
        return getUriFromCursor(mContext, mCursor);
    }

    /**
     * Gets the valid ringtone uri by a given uri string and ringtone type for the restore purpose.
     *
     * @param contentResolver ContentResolver to execute media query.
     * @param value a canonicalized uri which refers to the ringtone.
     * @param ringtoneType an integer representation of the kind of uri that is being restored, can
     *     be RingtoneManager.TYPE_RINGTONE, RingtoneManager.TYPE_NOTIFICATION, or
     *     RingtoneManager.TYPE_ALARM.
     * @hide
     */
    public static @Nullable Uri getRingtoneUriForRestore(
            @NonNull ContentResolver contentResolver, @Nullable String value, int ringtoneType)
            throws FileNotFoundException, IllegalArgumentException {
        if (value == null) {
            // Return a valid null. It means the null value is intended instead of a failure.
            return null;
        }

        Uri ringtoneUri;
        final Uri canonicalUri = Uri.parse(value);

        // Try to get the media uri via the regular uncanonicalize method first.
        ringtoneUri = contentResolver.uncanonicalize(canonicalUri);
        if (ringtoneUri != null) {
            // Canonicalize it to make the result contain the right metadata of the media asset.
            ringtoneUri = contentResolver.canonicalize(ringtoneUri);
            return ringtoneUri;
        }

        // Query the media by title and ringtone type.
        final String title = canonicalUri.getQueryParameter(AudioColumns.TITLE);
        Uri baseUri = ContentUris.removeId(canonicalUri).buildUpon().clearQuery().build();
        String ringtoneTypeSelection = "";
        switch (ringtoneType) {
            case RingtoneManager.TYPE_RINGTONE:
                ringtoneTypeSelection = MediaStore.Audio.AudioColumns.IS_RINGTONE;
                break;
            case RingtoneManager.TYPE_NOTIFICATION:
                ringtoneTypeSelection = MediaStore.Audio.AudioColumns.IS_NOTIFICATION;
                break;
            case RingtoneManager.TYPE_ALARM:
                ringtoneTypeSelection = MediaStore.Audio.AudioColumns.IS_ALARM;
                break;
            default:
                throw new IllegalArgumentException("Unknown ringtone type: " + ringtoneType);
        }

        final String selection = ringtoneTypeSelection + "=1 AND " + AudioColumns.TITLE + "=?";
        Cursor cursor = null;
        try {
            cursor =
                    contentResolver.query(
                            baseUri,
                            /* projection */ new String[] {BaseColumns._ID},
                            /* selection */ selection,
                            /* selectionArgs */ new String[] {title},
                            /* sortOrder */ null,
                            /* cancellationSignal */ null);

        } catch (IllegalArgumentException e) {
            throw new FileNotFoundException("Volume not found for " + baseUri);
        }
        if (cursor == null) {
            throw new FileNotFoundException("Missing cursor for " + baseUri);
        } else if (cursor.getCount() == 0) {
            FileUtils.closeQuietly(cursor);
            throw new FileNotFoundException("No item found for " + baseUri);
        } else if (cursor.getCount() > 1) {
            // Find more than 1 result.
            // We are not sure which one is the right ringtone file so just abandon this case.
            FileUtils.closeQuietly(cursor);
            throw new FileNotFoundException(
                    "Find multiple ringtone candidates by title+ringtone_type query: count: "
                            + cursor.getCount());
        }
        if (cursor.moveToFirst()) {
            ringtoneUri = ContentUris.withAppendedId(baseUri, cursor.getLong(0));
            FileUtils.closeQuietly(cursor);
        } else {
            FileUtils.closeQuietly(cursor);
            throw new FileNotFoundException("Failed to read row from the result.");
        }

        // Canonicalize it to make the result contain the right metadata of the media asset.
        ringtoneUri = contentResolver.canonicalize(ringtoneUri);
        Log.v(TAG, "Find a valid result: " + ringtoneUri);
        return ringtoneUri;
    }

    private static Uri getUriFromCursor(Context context, Cursor cursor) {
        final Uri uri = ContentUris.withAppendedId(Uri.parse(cursor.getString(URI_COLUMN_INDEX)),
                cursor.getLong(ID_COLUMN_INDEX));
+21 −11
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.LocalePicker;
import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager;

import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
@@ -332,21 +333,30 @@ public class SettingsHelper {
     * @param value can be a canonicalized uri or "_silent" to indicate a silent (null) ringtone.
     */
    private void setRingtone(String name, String value) {
        // If it's null, don't change the default
        Log.v(TAG, "Set ringtone for name: " + name + " value: " + value);

        // If it's null, don't change the default.
        if (value == null) return;
        final Uri ringtoneUri;
        final int ringtoneType = getRingtoneType(name);
        if (SILENT_RINGTONE.equals(value)) {
            ringtoneUri = null;
        } else {
            Uri canonicalUri = Uri.parse(value);
            ringtoneUri = mContext.getContentResolver().uncanonicalize(canonicalUri);
            if (ringtoneUri == null) {
                // Unrecognized or invalid Uri, don't restore
            // SILENT_RINGTONE is a special constant generated by onBackupValue in the source
            // device.
            RingtoneManager.setActualDefaultRingtoneUri(mContext, ringtoneType, null);
            return;
        }

        Uri ringtoneUri = null;
        try {
            ringtoneUri =
                    RingtoneManager.getRingtoneUriForRestore(
                            mContext.getContentResolver(), value, ringtoneType);
        } catch (FileNotFoundException | IllegalArgumentException e) {
            Log.w(TAG, "Failed to resolve " + value + ": " + e);
            // Unrecognized or invalid Uri, don't restore
            return;
        }
        final int ringtoneType = getRingtoneType(name);

        Log.v(TAG, "setActualDefaultRingtoneUri type: " + ringtoneType + ", uri: " + ringtoneUri);
        RingtoneManager.setActualDefaultRingtoneUri(mContext, ringtoneType, ringtoneUri);
    }

+404 −0

File changed.

Preview size limit exceeded, changes collapsed.

Loading