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
(cherry picked from commit c27bb6ad34)
Change-Id: I32c186d0d7479b01f6cc67cce9bc5cb66264a064
This commit is contained in:
committed by
Michal Karpinski
parent
43aca9cdc8
commit
2d7a4a3f67
@@ -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;
|
||||
@@ -26,6 +29,8 @@ import android.provider.Settings;
|
||||
import android.service.notification.NotificationListenerService;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
@@ -562,17 +567,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));
|
||||
@@ -584,11 +610,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) {
|
||||
@@ -609,8 +686,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()));
|
||||
|
||||
@@ -227,7 +227,11 @@ public class RankingHelper implements RankingConfig {
|
||||
if (!TextUtils.isEmpty(id) && !TextUtils.isEmpty(channelName)) {
|
||||
NotificationChannel channel = new NotificationChannel(id,
|
||||
channelName, channelImportance);
|
||||
channel.populateFromXml(parser);
|
||||
if (forRestore) {
|
||||
channel.populateFromXmlForRestore(parser, mContext);
|
||||
} else {
|
||||
channel.populateFromXml(parser);
|
||||
}
|
||||
r.channels.put(id, channel);
|
||||
}
|
||||
}
|
||||
@@ -390,7 +394,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -364,7 +390,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);
|
||||
@@ -425,6 +451,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");
|
||||
|
||||
Reference in New Issue
Block a user