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

Commit ba4b5739 authored by Wenhao Shi's avatar Wenhao Shi Committed by Cherrypicker Worker
Browse files

Fix custom ringtone restore in Notification channels.

Bug: 278169915
Test: atest PreferencesHelperTest && atest frameworks/base/core/tests/coretests/src/android/app/NotificationChannelTest.java
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:938eb0984d7672704ba3375d21b360010068f1c0)
Merged-In: I5c00b252054cf3736b3f24859a41d3fd894fe1de
Change-Id: I5c00b252054cf3736b3f24859a41d3fd894fe1de
parent 452ffae8
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);
    }
}
+6 −1
Original line number Diff line number Diff line
@@ -2608,7 +2608,12 @@ public class PreferencesHelper implements RankingConfig {
                            for (NotificationChannel channel : r.channels.values()) {
                                if (!channel.isSoundRestored()) {
                                    Uri uri = channel.getSound();
                                    Uri restoredUri = channel.restoreSoundUri(mContext, uri, true);
                                    Uri restoredUri =
                                            channel.restoreSoundUri(
                                                    mContext,
                                                    uri,
                                                    true,
                                                    channel.getAudioAttributes().getUsage());
                                    if (Settings.System.DEFAULT_NOTIFICATION_URI.equals(
                                            restoredUri)) {
                                        Log.w(TAG,
+4 −2
Original line number Diff line number Diff line
@@ -600,7 +600,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
        NotificationChannel channel2 =
                new NotificationChannel("id2", "name2", IMPORTANCE_LOW);
        channel2.setDescription("descriptions for all");
        channel2.setSound(SOUND_URI, mAudioAttributes);
        channel2.setSound(CANONICAL_SOUND_URI, mAudioAttributes);
        channel2.enableLights(true);
        channel2.setBypassDnd(true);
        channel2.setLockscreenVisibility(VISIBILITY_SECRET);
@@ -1374,6 +1374,8 @@ public class PreferencesHelperTest extends UiServiceTestCase {
                .when(mTestIContentProvider).uncanonicalize(any(), eq(CANONICAL_SOUND_URI));
        doReturn(localUri)
                .when(mTestIContentProvider).uncanonicalize(any(), eq(canonicalBasedOnLocal));
        doReturn(canonicalBasedOnLocal)
                .when(mTestIContentProvider).canonicalize(any(), eq(localUri));

        NotificationChannel channel =
                new NotificationChannel("id", "name", IMPORTANCE_LOW);
@@ -1387,7 +1389,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {

        NotificationChannel actualChannel = mHelper.getNotificationChannel(
                PKG_N_MR1, UID_N_MR1, channel.getId(), false);
        assertEquals(localUri, actualChannel.getSound());
        assertEquals(canonicalBasedOnLocal, actualChannel.getSound());
    }

    @Test