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

Commit 504e9271 authored by Matías Hernández's avatar Matías Hernández
Browse files

Better Support for profiles in "People that can interrupt"

* Show contacts from personal and work profile.
* Open personal or work profile Contacts app when choosing settings.
* Skip conversations with no ShortcutInfo (they are returned for a disabled work profile but we cannot show an icon for them).

Fixes: 371513451
Test: atest com.android.settings.notification.modes
Flag: android.app.modes_ui
Change-Id: Id8653a85ee4fd15dfccbecb3ea2d31e615d29f8c
parent aad13d88
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -71,7 +71,9 @@ public class UserAdapter extends BaseAdapter {
                        && userInfo.isPrivateProfile())) {
                mIcon = context.getPackageManager().getUserBadgeForDensityNoBackground(
                        userHandle, /* density= */ 0);
                if (mIcon != null) {
                    mIcon.setTint(tintColor);
                }
            } else {
                mIcon = UserIcons.getDefaultUserIconInColor(context.getResources(), tintColor);
            }
+42 −17
Original line number Diff line number Diff line
@@ -18,13 +18,17 @@ package com.android.settings.notification.modes;

import android.annotation.Nullable;
import android.app.INotificationManager;
import android.content.ContentProvider;
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.service.notification.ConversationChannelWrapper;
import android.util.Log;

@@ -39,6 +43,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

/**
 * Class used for Settings-system_server interactions that are not <em>directly</em> related to
@@ -54,6 +59,7 @@ class ZenHelperBackend {

    private final Context mContext;
    private final INotificationManager mInm;
    private final UserManager mUserManager;

    static ZenHelperBackend getInstance(Context context) {
        if (sInstance == null) {
@@ -66,6 +72,7 @@ class ZenHelperBackend {
        mContext = context;
        mInm = INotificationManager.Stub.asInterface(
                ServiceManager.getService(Context.NOTIFICATION_SERVICE));
        mUserManager = context.getSystemService(UserManager.class);
    }

    /**
@@ -81,10 +88,12 @@ class ZenHelperBackend {
        }
    }

    /** Returns all conversation channels for profiles of the current user. */
    ImmutableList<ConversationChannelWrapper> getAllConversations() {
        return getConversations(false);
    }

    /** Returns all important (priority) conversation channels for profiles of the current user. */
    ImmutableList<ConversationChannelWrapper> getImportantConversations() {
        return getConversations(true);
    }
@@ -97,7 +106,9 @@ class ZenHelperBackend {
                    onlyImportant);
            if (parceledList != null) {
                for (ConversationChannelWrapper conversation : parceledList.getList()) {
                    if (!conversation.getNotificationChannel().isDemoted()) {
                    if (conversation.getShortcutInfo() != null
                            && conversation.getNotificationChannel() != null
                            && !conversation.getNotificationChannel().isDemoted()) {
                        list.add(conversation);
                    }
                }
@@ -109,38 +120,52 @@ class ZenHelperBackend {
        }
    }

    record Contact(long id, @Nullable String displayName, @Nullable Uri photoUri) { }
    record Contact(UserHandle user, long contactId, @Nullable String displayName,
                   @Nullable Uri photoUri) { }

    /** Returns all contacts for profiles of the current user. */
    ImmutableList<Contact> getAllContacts() {
        try (Cursor cursor = queryAllContactsData()) {
            return getContactsFromCursor(cursor);
        }
        return getContactsForUserProfiles(this::queryAllContactsData);
    }

    /** Returns all starred contacts for profiles of the current user. */
    ImmutableList<Contact> getStarredContacts() {
        try (Cursor cursor = queryStarredContactsData()) {
            return getContactsFromCursor(cursor);
        return getContactsForUserProfiles(this::queryStarredContactsData);
    }

    private ImmutableList<Contact> getContactsForUserProfiles(
            Function<UserHandle, Cursor> userQuery) {
        ImmutableList.Builder<Contact> contacts = new ImmutableList.Builder<>();
        for (UserHandle user : mUserManager.getAllProfiles()) {
            try (Cursor cursor = userQuery.apply(user)) {
                loadContactsFromCursor(user, cursor, contacts);
            }
        }
        return contacts.build();
    }

    private ImmutableList<Contact> getContactsFromCursor(Cursor cursor) {
        ImmutableList.Builder<Contact> list = new ImmutableList.Builder<>();
    private void loadContactsFromCursor(UserHandle user, Cursor cursor,
            ImmutableList.Builder<Contact> contactsListBuilder) {
        if (cursor != null && cursor.moveToFirst()) {
            do {
                long id = cursor.getLong(0);
                String name = Strings.emptyToNull(cursor.getString(1));
                String photoUriStr = cursor.getString(2);
                Uri photoUri = !Strings.isNullOrEmpty(photoUriStr) ? Uri.parse(photoUriStr) : null;
                list.add(new Contact(id, name, photoUri));
                contactsListBuilder.add(new Contact(user, id, name,
                        ContentProvider.maybeAddUserId(photoUri, user.getIdentifier())));
            } while (cursor.moveToNext());
        }
        return list.build();
    }

    int getAllContactsCount() {
        try (Cursor cursor = queryAllContactsData()) {
            return cursor != null ? cursor.getCount() : 0;
        int count = 0;
        for (UserHandle user : mUserManager.getEnabledProfiles()) {
            try (Cursor cursor = queryAllContactsData(user)) {
                count += (cursor != null ? cursor.getCount() : 0);
            }
        }
        return count;
    }

    private static final String[] CONTACTS_PROJECTION = new String[] {
@@ -149,17 +174,17 @@ class ZenHelperBackend {
            ContactsContract.Contacts.PHOTO_THUMBNAIL_URI
    };

    private Cursor queryStarredContactsData() {
    private Cursor queryStarredContactsData(UserHandle user) {
        return mContext.getContentResolver().query(
                ContactsContract.Contacts.CONTENT_URI,
                ContentProvider.maybeAddUserId(Contacts.CONTENT_URI, user.getIdentifier()),
                CONTACTS_PROJECTION,
                /* selection= */ ContactsContract.Data.STARRED + "=1", /* selectionArgs= */ null,
                /* sortOrder= */ ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
    }

    private Cursor queryAllContactsData() {
    private Cursor queryAllContactsData(UserHandle user) {
        return mContext.getContentResolver().query(
                ContactsContract.Contacts.CONTENT_URI,
                ContentProvider.maybeAddUserId(Contacts.CONTENT_URI, user.getIdentifier()),
                CONTACTS_PROJECTION,
                /* selection= */ null, /* selectionArgs= */ null,
                /* sortOrder= */ ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
+38 −14
Original line number Diff line number Diff line
@@ -28,11 +28,14 @@ import static android.service.notification.ZenPolicy.PEOPLE_TYPE_UNSET;

import static com.google.common.base.Preconditions.checkNotNull;

import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.icu.text.MessageFormat;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Contacts;
import android.service.notification.ZenPolicy;
import android.view.View;
@@ -46,6 +49,7 @@ import androidx.preference.PreferenceScreen;

import com.android.settings.R;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.profileselector.ProfileSelectDialog;
import com.android.settings.notification.app.ConversationListSettings;
import com.android.settingslib.notification.modes.ZenMode;
import com.android.settingslib.notification.modes.ZenModesBackend;
@@ -55,6 +59,7 @@ import com.google.common.collect.ImmutableSet;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -87,16 +92,18 @@ class ZenModePrioritySendersPreferenceController
    private static final Intent STARRED_CONTACTS_INTENT =
            new Intent(Contacts.Intents.UI.LIST_STARRED_ACTION)
                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK  | Intent.FLAG_ACTIVITY_CLEAR_TASK);
    private static final Intent FALLBACK_INTENT = new Intent(Intent.ACTION_MAIN)
    private static final Intent FALLBACK_CONTACTS_INTENT = new Intent(Intent.ACTION_MAIN)
            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);

    private final ZenHelperBackend mHelperBackend;
    private final UserManager mUserManager;
    private final PackageManager mPackageManager;
    private PreferenceCategory mPreferenceCategory;
    private final LinkedHashMap<String, SelectorWithWidgetPreference> mOptions =
            new LinkedHashMap<>();

    private final ZenModeSummaryHelper mZenModeSummaryHelper;
    @Nullable private Dialog mProfileSelectDialog;

    public ZenModePrioritySendersPreferenceController(Context context, String key,
            boolean isMessages, ZenModesBackend backend, ZenHelperBackend helperBackend) {
@@ -107,11 +114,12 @@ class ZenModePrioritySendersPreferenceController
        String contactsPackage = context.getString(R.string.config_contacts_package_name);
        ALL_CONTACTS_INTENT.setPackage(contactsPackage);
        STARRED_CONTACTS_INTENT.setPackage(contactsPackage);
        FALLBACK_INTENT.setPackage(contactsPackage);
        FALLBACK_CONTACTS_INTENT.setPackage(contactsPackage);

        mUserManager = mContext.getSystemService(UserManager.class);
        mPackageManager = mContext.getPackageManager();
        if (!FALLBACK_INTENT.hasCategory(Intent.CATEGORY_APP_CONTACTS)) {
            FALLBACK_INTENT.addCategory(Intent.CATEGORY_APP_CONTACTS);
        if (!FALLBACK_CONTACTS_INTENT.hasCategory(Intent.CATEGORY_APP_CONTACTS)) {
            FALLBACK_CONTACTS_INTENT.addCategory(Intent.CATEGORY_APP_CONTACTS);
        }
        mZenModeSummaryHelper = new ZenModeSummaryHelper(mContext, mHelperBackend);
    }
@@ -270,32 +278,48 @@ class ZenModePrioritySendersPreferenceController
        }

        return v -> {
            if (KEY_STARRED.equals(key)
                    && STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) {
                mContext.startActivity(STARRED_CONTACTS_INTENT);
            } else if (KEY_CONTACTS.equals(key)
                    && ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) {
                mContext.startActivity(ALL_CONTACTS_INTENT);
            if (KEY_STARRED.equals(key)) {
                startContactsActivity(STARRED_CONTACTS_INTENT);
            } else if (KEY_CONTACTS.equals(key)) {
                startContactsActivity(ALL_CONTACTS_INTENT);
            } else if (KEY_ANY_CONVERSATIONS.equals(key)
                    || KEY_IMPORTANT_CONVERSATIONS.equals(key)) {
                new SubSettingLauncher(mContext)
                        .setDestination(ConversationListSettings.class.getName())
                        .setSourceMetricsCategory(SettingsEnums.DND_MESSAGES)
                        .launch();
            } else {
                mContext.startActivity(FALLBACK_INTENT);
            }
        };
    }

    private void startContactsActivity(Intent preferredIntent) {
        Intent intent = preferredIntent.resolveActivity(mPackageManager) != null
                ? preferredIntent : FALLBACK_CONTACTS_INTENT;

        List<UserHandle> userProfiles = mUserManager.getEnabledProfiles();
        if (userProfiles.size() <= 1) {
            mContext.startActivity(intent);
        }

        mProfileSelectDialog = ProfileSelectDialog.createDialog(mContext, userProfiles,
                position -> {
                    mContext.startActivityAsUser(intent, userProfiles.get(position));
                    if (mProfileSelectDialog != null) {
                        mProfileSelectDialog.dismiss();
                        mProfileSelectDialog = null;
                    }
                });
        mProfileSelectDialog.show();
    }

    private boolean isStarredIntentValid() {
        return STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null
                || FALLBACK_INTENT.resolveActivity(mPackageManager) != null;
                || FALLBACK_CONTACTS_INTENT.resolveActivity(mPackageManager) != null;
    }

    private boolean isContactsIntentValid() {
        return ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null
                || FALLBACK_INTENT.resolveActivity(mPackageManager) != null;
                || FALLBACK_CONTACTS_INTENT.resolveActivity(mPackageManager) != null;
    }

    void updateSummaries() {
+260 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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 com.android.settings.notification.modes;

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

import static org.robolectric.Shadows.shadowOf;

import android.app.Flags;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.content.pm.UserInfo;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.ContactsContract;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.settings.notification.modes.ZenHelperBackend.Contact;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;

@RunWith(RobolectricTestRunner.class)
@EnableFlags(Flags.FLAG_MODES_UI)
public class ZenHelperBackendTest {
    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    private Context mContext;
    private ZenHelperBackend mBackend;
    private HashMap<Integer, FakeContactsProvider> mContactsProviders = new HashMap<>();

    private int mUserId;

    @Before
    public void setUp() {
        mContext = RuntimeEnvironment.getApplication();
        mBackend = new ZenHelperBackend(mContext);

        mUserId = mContext.getUserId();
        addContactsProvider(mUserId);
    }

    private int addMainUserProfile() {
        UserInfo workProfile = new UserInfo(mUserId + 10, "Work Profile", 0);
        workProfile.userType = UserManager.USER_TYPE_PROFILE_MANAGED;
        UserManager userManager = mContext.getSystemService(UserManager.class);
        shadowOf(userManager).addProfile(mUserId, workProfile.id, workProfile);

        addContactsProvider(workProfile.id);

        return workProfile.id;
    }

    private void addContactsProvider(int userId) {
        ProviderInfo providerInfo = new ProviderInfo();
        providerInfo.authority = String.format("%s@%s", userId, ContactsContract.AUTHORITY);
        mContactsProviders.put(userId, Robolectric.buildContentProvider(FakeContactsProvider.class)
                .create(providerInfo).get());
    }

    private void addContact(int userId, String name, boolean starred) {
        mContactsProviders.get(userId).addContact(name, starred);
    }

    @Test
    public void getAllContacts_singleProfile() {
        addContact(mUserId, "Huey", false);
        addContact(mUserId, "Dewey", true);
        addContact(mUserId, "Louie", false);

        ImmutableList<Contact> allContacts = mBackend.getAllContacts();

        assertThat(allContacts).containsExactly(
                new Contact(UserHandle.of(mUserId), 1, "Huey", null),
                new Contact(UserHandle.of(mUserId), 2, "Dewey", null),
                new Contact(UserHandle.of(mUserId), 3, "Louie", null));
    }

    @Test
    public void getAllContacts_multipleProfiles() {
        int profileId = addMainUserProfile();
        addContact(mUserId, "Huey", false);
        addContact(mUserId, "Dewey", true);
        addContact(mUserId, "Louie", false);
        addContact(profileId, "Fry", false);
        addContact(profileId, "Bender", true);

        ImmutableList<Contact> allContacts = mBackend.getAllContacts();

        assertThat(allContacts).containsExactly(
                new Contact(UserHandle.of(mUserId), 1, "Huey", null),
                new Contact(UserHandle.of(mUserId), 2, "Dewey", null),
                new Contact(UserHandle.of(mUserId), 3, "Louie", null),
                new Contact(UserHandle.of(profileId), 1, "Fry", null),
                new Contact(UserHandle.of(profileId), 2, "Bender", null));
    }

    @Test
    public void getStarredContacts_singleProfile() {
        addContact(mUserId, "Huey", false);
        addContact(mUserId, "Dewey", true);
        addContact(mUserId, "Louie", false);

        ImmutableList<Contact> allContacts = mBackend.getStarredContacts();

        assertThat(allContacts).containsExactly(
                new Contact(UserHandle.of(mUserId), 2, "Dewey", null));
    }

    @Test
    public void getStarredContacts_multipleProfiles() {
        int profileId = addMainUserProfile();
        addContact(mUserId, "Huey", false);
        addContact(mUserId, "Dewey", true);
        addContact(mUserId, "Louie", false);
        addContact(profileId, "Fry", false);
        addContact(profileId, "Bender", true);

        ImmutableList<Contact> allContacts = mBackend.getStarredContacts();

        assertThat(allContacts).containsExactly(
                new Contact(UserHandle.of(mUserId), 2, "Dewey", null),
                new Contact(UserHandle.of(profileId), 2, "Bender", null));
    }

    @Test
    public void getAllContactsCount_singleProfile() {
        addContact(mUserId, "Huey", false);
        addContact(mUserId, "Dewey", true);
        addContact(mUserId, "Louie", false);

        assertThat(mBackend.getAllContactsCount()).isEqualTo(3);
    }

    @Test
    public void getAllContactsCount_multipleProfiles() {
        int profileId = addMainUserProfile();
        addContact(mUserId, "Huey", false);
        addContact(mUserId, "Dewey", true);
        addContact(mUserId, "Louie", false);
        addContact(profileId, "Fry", false);
        addContact(profileId, "Bender", true);

        assertThat(mBackend.getAllContactsCount()).isEqualTo(5);
    }

    private static class FakeContactsProvider extends ContentProvider {

        private record ContactRow(int id, String name, boolean starred) {}

        private final ArrayList<ContactRow> mContacts = new ArrayList<>();

        FakeContactsProvider() {
        }

        @Override
        public boolean onCreate() {
            return true;
        }

        public int addContact(String name, boolean starred) {
            mContacts.add(new ContactRow(mContacts.size() + 1, name, starred));
            return mContacts.size();
        }

        @Nullable
        @Override
        public Cursor query(@NonNull Uri uri, @Nullable String[] projection,
                @Nullable String selection, @Nullable String[] selectionArgs,
                @Nullable String sortOrder) {
            Uri baseUri = ContentProvider.getUriWithoutUserId(uri);
            if (!ContactsContract.Contacts.CONTENT_URI.equals(baseUri)) {
                throw new IllegalArgumentException("Unsupported uri for fake: " + uri);
            }

            if (projection == null || !Iterables.elementsEqual(ImmutableList.copyOf(projection),
                    ImmutableList.of(ContactsContract.Contacts._ID,
                            ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
                            ContactsContract.Contacts.PHOTO_THUMBNAIL_URI))) {
                throw new IllegalArgumentException(
                        "Unsupported projection for fake: " + Arrays.toString(projection));
            }

            if (selection != null && !selection.equals(ContactsContract.Data.STARRED + "=1")) {
                throw new IllegalArgumentException("Unsupported selection for fake: " + selection);
            }
            boolean selectingStarred = selection != null; // Checked as only valid selection above


            MatrixCursor cursor = new MatrixCursor(projection);
            for (ContactRow contactRow : mContacts) {
                if (!selectingStarred || contactRow.starred) {
                    cursor.addRow(ImmutableList.of(contactRow.id, contactRow.name, Uri.EMPTY));
                }
            }

            return cursor;
        }

        @Override
        @Nullable
        public String getType(@NonNull Uri uri) {
            return "";
        }

        @Nullable
        @Override
        public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
            throw new UnsupportedOperationException();
        }

        @Override
        public int delete(@NonNull Uri uri, @Nullable String selection,
                @Nullable String[] selectionArgs) {
            throw new UnsupportedOperationException();
        }

        @Override
        public int update(@NonNull Uri uri, @Nullable ContentValues values,
                @Nullable String selection, @Nullable String[] selectionArgs) {
            throw new UnsupportedOperationException();
        }
    }
}
+6 −5
Original line number Diff line number Diff line
@@ -43,6 +43,7 @@ import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.UserHandle;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ConversationChannelWrapper;
@@ -229,13 +230,13 @@ public final class ZenModePeopleLinkPreferenceControllerTest {

    private void setUpContacts(Collection<Integer> allIds, Collection<Integer> starredIds) {
        when(mHelperBackend.getAllContacts()).thenReturn(ImmutableList.copyOf(
                allIds.stream()
                        .map(id -> new Contact(id, "#" + id, Uri.parse("photo://" + id)))
                allIds.stream().map(id -> new Contact(UserHandle.SYSTEM, id, "#" + id,
                                Uri.parse("photo://" + id)))
                        .toList()));

        when(mHelperBackend.getStarredContacts()).thenReturn(ImmutableList.copyOf(
                starredIds.stream()
                        .map(id -> new Contact(id, "#" + id, Uri.parse("photo://" + id)))
                starredIds.stream().map(id -> new Contact(UserHandle.SYSTEM, id, "#" + id,
                                Uri.parse("photo://" + id)))
                        .toList()));
    }

@@ -253,6 +254,6 @@ public final class ZenModePeopleLinkPreferenceControllerTest {
    }

    private static ColorDrawable photoOf(Contact contact) {
        return new ColorDrawable((int) contact.id());
        return new ColorDrawable((int) contact.contactId());
    }
}
 No newline at end of file
Loading