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

Commit c27bb6ad authored by Bernardo Rufino's avatar Bernardo Rufino
Browse files

Canonicalize notification channel sounds for backup

Canonicalize for backup and canonicalize and uncanonicalize for restore
(see comment).

Test: Set custom notification sound, make backup, remove notification
sound from device (from Ringtones and make sure to update media content
provider), restore => Observe default instead of random number. Do the
same without removing the sound and observe restores successfully.
Test: runtest systemui-notification
Bug: 66444697
Change-Id: Id2f4685b7f64a35e397eb6dff5246802d795d996
parent c4381465
Loading
Loading
Loading
Loading
+81 −3
Original line number Diff line number Diff line
@@ -15,8 +15,11 @@
 */
package android.app;

import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.app.NotificationManager.Importance;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.media.AudioAttributes;
import android.net.Uri;
@@ -27,6 +30,8 @@ import android.service.notification.NotificationListenerService;
import android.text.TextUtils;
import android.util.proto.ProtoOutputStream;

import com.android.internal.util.Preconditions;

import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
@@ -566,17 +571,38 @@ public final class NotificationChannel implements Parcelable {
        return mBlockableSystem;
    }

    /**
     * @hide
     */
    public void populateFromXmlForRestore(XmlPullParser parser, Context context) {
        populateFromXml(parser, true, context);
    }

    /**
     * @hide
     */
    @SystemApi
    public void populateFromXml(XmlPullParser parser) {
        populateFromXml(parser, false, null);
    }

    /**
     * If {@param forRestore} is true, {@param Context} MUST be non-null.
     */
    private void populateFromXml(XmlPullParser parser, boolean forRestore,
            @Nullable Context context) {
        Preconditions.checkArgument(!forRestore || context != null,
                "forRestore is true but got null context");

        // Name, id, and importance are set in the constructor.
        setDescription(parser.getAttributeValue(null, ATT_DESC));
        setBypassDnd(Notification.PRIORITY_DEFAULT
                != safeInt(parser, ATT_PRIORITY, Notification.PRIORITY_DEFAULT));
        setLockscreenVisibility(safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY));
        setSound(safeUri(parser, ATT_SOUND), safeAudioAttributes(parser));

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

        enableLights(safeBool(parser, ATT_LIGHTS, false));
        setLightColor(safeInt(parser, ATT_LIGHT_COLOR, DEFAULT_LIGHT_COLOR));
        setVibrationPattern(safeLongArray(parser, ATT_VIBRATION, null));
@@ -588,11 +614,62 @@ public final class NotificationChannel implements Parcelable {
        setBlockableSystem(safeBool(parser, ATT_BLOCKABLE_SYSTEM, false));
    }

    @Nullable
    private Uri restoreSoundUri(Context context, @Nullable Uri uri) {
        if (uri == null) {
            return null;
        }
        ContentResolver contentResolver = context.getContentResolver();
        // There are backups out there with uncanonical uris (because we fixed this after
        // shipping). If uncanonical uris are given to MediaProvider.uncanonicalize it won't
        // verify the uri against device storage and we'll possibly end up with a broken uri.
        // We then canonicalize the uri to uncanonicalize it back, which means we properly check
        // the uri and in the case of not having the resource we end up with the default - better
        // than broken. As a side effect we'll canonicalize already canonicalized uris, this is fine
        // according to the docs because canonicalize method has to handle canonical uris as well.
        Uri canonicalizedUri = contentResolver.canonicalize(uri);
        if (canonicalizedUri == null) {
            // We got a null because the uri in the backup does not exist here, so we return default
            return Settings.System.DEFAULT_NOTIFICATION_URI;
        }
        return contentResolver.uncanonicalize(canonicalizedUri);
    }

    /**
     * @hide
     */
    @SystemApi
    public void writeXml(XmlSerializer out) throws IOException {
        writeXml(out, false, null);
    }

    /**
     * @hide
     */
    public void writeXmlForBackup(XmlSerializer out, Context context) throws IOException {
        writeXml(out, true, context);
    }

    private Uri getSoundForBackup(Context context) {
        Uri sound = getSound();
        if (sound == null) {
            return null;
        }
        Uri canonicalSound = context.getContentResolver().canonicalize(sound);
        if (canonicalSound == null) {
            // The content provider does not support canonical uris so we backup the default
            return Settings.System.DEFAULT_NOTIFICATION_URI;
        }
        return canonicalSound;
    }

    /**
     * If {@param forBackup} is true, {@param Context} MUST be non-null.
     */
    private void writeXml(XmlSerializer out, boolean forBackup, @Nullable Context context)
            throws IOException {
        Preconditions.checkArgument(!forBackup || context != null,
                "forBackup is true but got null context");
        out.startTag(null, TAG_CHANNEL);
        out.attribute(null, ATT_ID, getId());
        if (getName() != null) {
@@ -613,8 +690,9 @@ public final class NotificationChannel implements Parcelable {
            out.attribute(null, ATT_VISIBILITY,
                    Integer.toString(getLockscreenVisibility()));
        }
        if (getSound() != null) {
            out.attribute(null, ATT_SOUND, getSound().toString());
        Uri sound = forBackup ? getSoundForBackup(context) : getSound();
        if (sound != null) {
            out.attribute(null, ATT_SOUND, sound.toString());
        }
        if (getAudioAttributes() != null) {
            out.attribute(null, ATT_USAGE, Integer.toString(getAudioAttributes().getUsage()));
+10 −2
Original line number Diff line number Diff line
@@ -231,7 +231,11 @@ public class RankingHelper implements RankingConfig {
                                if (!TextUtils.isEmpty(id) && !TextUtils.isEmpty(channelName)) {
                                    NotificationChannel channel = new NotificationChannel(id,
                                            channelName, channelImportance);
                                    if (forRestore) {
                                        channel.populateFromXmlForRestore(parser, mContext);
                                    } else {
                                        channel.populateFromXml(parser);
                                    }
                                    r.channels.put(id, channel);
                                }
                            }
@@ -394,7 +398,11 @@ public class RankingHelper implements RankingConfig {
                    }

                    for (NotificationChannel channel : r.channels.values()) {
                        if (!forBackup || (forBackup && !channel.isDeleted())) {
                        if (forBackup) {
                            if (!channel.isDeleted()) {
                                channel.writeXmlForBackup(out, mContext);
                            }
                        } else {
                            channel.writeXml(out);
                        }
                    }
+149 −20
Original line number Diff line number Diff line
@@ -25,25 +25,13 @@ import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.fail;

import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;

import com.android.internal.util.FastXmlSerializer;

import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;

import android.app.Notification;
import android.app.NotificationChannelGroup;
import android.content.Context;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.content.ContentProvider;
import android.content.Context;
import android.content.IContentProvider;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
@@ -52,14 +40,28 @@ import android.media.AudioAttributes;
import android.net.Uri;
import android.os.Build;
import android.os.UserHandle;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.service.notification.StatusBarNotification;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.TestableContentResolver;
import android.util.ArrayMap;
import android.util.Xml;

import com.android.internal.util.FastXmlSerializer;

import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
@@ -76,6 +78,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
@@ -95,10 +98,17 @@ public class RankingHelperTest extends NotificationTestCase {
    private static final int UID2 = 1111;
    private static final UserHandle USER2 = UserHandle.of(10);
    private static final String TEST_CHANNEL_ID = "test_channel_id";
    private static final String TEST_AUTHORITY = "test";
    private static final Uri SOUND_URI =
            Uri.parse("content://" + TEST_AUTHORITY + "/internal/audio/media/10");
    private static final Uri CANONICAL_SOUND_URI =
            Uri.parse("content://" + TEST_AUTHORITY
                    + "/internal/audio/media/10?title=Test&canonical=1");

    @Mock NotificationUsageStats mUsageStats;
    @Mock RankingHandler mHandler;
    @Mock PackageManager mPm;
    @Mock IContentProvider mTestIContentProvider;
    @Mock Context mContext;

    private Notification mNotiGroupGSortA;
@@ -134,9 +144,22 @@ public class RankingHelperTest extends NotificationTestCase {
        when(mContext.getPackageManager()).thenReturn(mPm);
        when(mContext.getApplicationInfo()).thenReturn(legacy);
        // most tests assume badging is enabled
        Secure.putIntForUser(getContext().getContentResolver(),
        TestableContentResolver contentResolver = getContext().getContentResolver();
        contentResolver.setFallbackToExisting(false);
        Secure.putIntForUser(contentResolver,
                Secure.NOTIFICATION_BADGING, 1, UserHandle.getUserId(UID));

        ContentProvider testContentProvider = mock(ContentProvider.class);
        when(testContentProvider.getIContentProvider()).thenReturn(mTestIContentProvider);
        contentResolver.addProvider(TEST_AUTHORITY, testContentProvider);

        when(mTestIContentProvider.canonicalize(any(), eq(SOUND_URI)))
                .thenReturn(CANONICAL_SOUND_URI);
        when(mTestIContentProvider.canonicalize(any(), eq(CANONICAL_SOUND_URI)))
                .thenReturn(CANONICAL_SOUND_URI);
        when(mTestIContentProvider.uncanonicalize(any(), eq(CANONICAL_SOUND_URI)))
                .thenReturn(SOUND_URI);

        mHelper = new RankingHelper(getContext(), mPm, mHandler, mUsageStats,
                new String[] {ImportanceExtractor.class.getName()});

@@ -214,9 +237,12 @@ public class RankingHelperTest extends NotificationTestCase {
    }

    private void loadStreamXml(ByteArrayOutputStream stream, boolean forRestore) throws Exception {
        loadByteArrayXml(stream.toByteArray(), forRestore);
    }

    private void loadByteArrayXml(byte[] byteArray, boolean forRestore) throws Exception {
        XmlPullParser parser = Xml.newPullParser();
        parser.setInput(new BufferedInputStream(new ByteArrayInputStream(stream.toByteArray())),
                null);
        parser.setInput(new BufferedInputStream(new ByteArrayInputStream(byteArray)), null);
        parser.nextTag();
        mHelper.readXml(parser, forRestore);
    }
@@ -377,7 +403,7 @@ public class RankingHelperTest extends NotificationTestCase {
        NotificationChannel channel2 =
                new NotificationChannel("id2", "name2", IMPORTANCE_LOW);
        channel2.setDescription("descriptions for all");
        channel2.setSound(new Uri.Builder().scheme("test").build(), mAudioAttributes);
        channel2.setSound(SOUND_URI, mAudioAttributes);
        channel2.enableLights(true);
        channel2.setBypassDnd(true);
        channel2.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
@@ -438,6 +464,109 @@ public class RankingHelperTest extends NotificationTestCase {
        assertTrue(foundChannel2Group);
    }

    @Test
    public void testBackupXml_backupCanonicalizedSoundUri() throws Exception {
        NotificationChannel channel =
                new NotificationChannel("id", "name", IMPORTANCE_LOW);
        channel.setSound(SOUND_URI, mAudioAttributes);
        mHelper.createNotificationChannel(PKG, UID, channel, true);

        ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel.getId());

        // Testing that in restore we are given the canonical version
        loadStreamXml(baos, true);
        verify(mTestIContentProvider).uncanonicalize(any(), eq(CANONICAL_SOUND_URI));
    }

    @Test
    public void testRestoreXml_withExistentCanonicalizedSoundUri() throws Exception {
        Uri localUri = Uri.parse("content://" + TEST_AUTHORITY + "/local/url");
        Uri canonicalBasedOnLocal = localUri.buildUpon()
                .appendQueryParameter("title", "Test")
                .appendQueryParameter("canonical", "1")
                .build();
        when(mTestIContentProvider.canonicalize(any(), eq(CANONICAL_SOUND_URI)))
                .thenReturn(canonicalBasedOnLocal);
        when(mTestIContentProvider.uncanonicalize(any(), eq(CANONICAL_SOUND_URI)))
                .thenReturn(localUri);
        when(mTestIContentProvider.uncanonicalize(any(), eq(canonicalBasedOnLocal)))
                .thenReturn(localUri);

        NotificationChannel channel =
                new NotificationChannel("id", "name", IMPORTANCE_LOW);
        channel.setSound(SOUND_URI, mAudioAttributes);
        mHelper.createNotificationChannel(PKG, UID, channel, true);
        ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel.getId());

        loadStreamXml(baos, true);

        NotificationChannel actualChannel = mHelper.getNotificationChannel(
                PKG, UID, channel.getId(), false);
        assertEquals(localUri, actualChannel.getSound());
    }

    @Test
    public void testRestoreXml_withNonExistentCanonicalizedSoundUri() throws Exception {
        Thread.sleep(3000);
        when(mTestIContentProvider.canonicalize(any(), eq(CANONICAL_SOUND_URI)))
                .thenReturn(null);
        when(mTestIContentProvider.uncanonicalize(any(), eq(CANONICAL_SOUND_URI)))
                .thenReturn(null);

        NotificationChannel channel =
                new NotificationChannel("id", "name", IMPORTANCE_LOW);
        channel.setSound(SOUND_URI, mAudioAttributes);
        mHelper.createNotificationChannel(PKG, UID, channel, true);
        ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel.getId());

        loadStreamXml(baos, true);

        NotificationChannel actualChannel = mHelper.getNotificationChannel(
                PKG, UID, channel.getId(), false);
        assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, actualChannel.getSound());
    }


    /**
     * Although we don't make backups with uncanonicalized uris anymore, we used to, so we have to
     * handle its restore properly.
     */
    @Test
    public void testRestoreXml_withUncanonicalizedNonLocalSoundUri() throws Exception {
        // Not a local uncanonicalized uri, simulating that it fails to exist locally
        when(mTestIContentProvider.canonicalize(any(), eq(SOUND_URI))).thenReturn(null);
        String id = "id";
        String backupWithUncanonicalizedSoundUri = "<ranking version=\"1\">\n"
                + "<package name=\"com.android.server.notification\" show_badge=\"true\">\n"
                + "<channel id=\"" + id + "\" name=\"name\" importance=\"2\" "
                + "sound=\"" + SOUND_URI + "\" "
                + "usage=\"6\" content_type=\"0\" flags=\"1\" show_badge=\"true\" />\n"
                + "<channel id=\"miscellaneous\" name=\"Uncategorized\" usage=\"5\" "
                + "content_type=\"4\" flags=\"0\" show_badge=\"true\" />\n"
                + "</package>\n"
                + "</ranking>\n";

        loadByteArrayXml(backupWithUncanonicalizedSoundUri.getBytes(), true);

        NotificationChannel actualChannel = mHelper.getNotificationChannel(PKG, UID, id, false);
        assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, actualChannel.getSound());
    }

    @Test
    public void testBackupRestoreXml_withNullSoundUri() throws Exception {
        NotificationChannel channel =
                new NotificationChannel("id", "name", IMPORTANCE_LOW);
        channel.setSound(null, mAudioAttributes);
        mHelper.createNotificationChannel(PKG, UID, channel, true);
        ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, true, channel.getId());

        loadStreamXml(baos, true);

        NotificationChannel actualChannel = mHelper.getNotificationChannel(
                PKG, UID, channel.getId(), false);
        assertEquals(null, actualChannel.getSound());
    }

    @Test
    public void testChannelXml_backup() throws Exception {
        NotificationChannelGroup ncg = new NotificationChannelGroup("1", "bye");