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

Commit 20814ed5 authored by Julia Reynolds's avatar Julia Reynolds
Browse files

Add package filtering to NLSes

Now users can prevent a given notification listener from seeing
notifications from particular apps.

Test: settings unit tests
Bug: 173052211
Change-Id: Ia868d6dc2da1ae7f75c0dca47034e28910584acd
parent 7c041874
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
@@ -8774,6 +8774,22 @@
    <string name="notif_type_alerting">Alerting notifications</string>
    <string name="notif_type_silent">Silent notifications</string>
    <!-- Per notification listener, launches a list of apps whose notifications this listener cannot see -->
    <string name="notif_listener_excluded_title">Apps that are not bridged to this listener</string>
    <!-- Per notification listener, when the listener can see notifications from all apps -->
    <string name="notif_listener_excluded_summary_zero">All apps are bridged</string>
    <!-- Per notification listener, a summary of how many apps this listener cannot see
     notifications from -->
    <plurals name="notif_listener_excluded_summary_nonzero">
        <item quantity="one">%d app is not bridged</item>
        <item quantity="other">%d apps are not bridged</item>
    </plurals>
    <!-- Per notification listener, a list of apps whose notifications this listener cannot see -->
    <string name="notif_listener_excluded_app_title">Bridged apps</string>
    <!-- Title for managing VR (virtual reality) helper services. [CHAR LIMIT=50] -->
    <string name="vr_listeners_title">VR helper services</string>
+25 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  Copyright (C) 2021 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.
  -->

<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:settings="http://schemas.android.com/apk/res-auto"
    android:key="nonbridged_apps"
    android:title="@string/notif_listener_excluded_app_title"
    settings:controller="com.android.settings.applications.specialaccess.notificationaccess.BridgedAppsPreferenceController"
    settings:searchable="false">
</PreferenceScreen>
+6 −6
Original line number Diff line number Diff line
@@ -41,11 +41,11 @@
        style="@style/SettingsMultiSelectListPreference"
        settings:controller="com.android.settings.applications.specialaccess.notificationaccess.TypeFilterPreferenceController"/>/>

    <PreferenceCategory
        android:key="advanced"
        android:order="50"
        settings:initialExpandedChildrenCount="0">
        <Preference
            android:key="bridged_apps"
            android:title="@string/notif_listener_excluded_app_title"
            android:fragment="com.android.settings.applications.specialaccess.notificationaccess.BridgedAppsSettings"
            settings:searchable="false"
            settings:controller="com.android.settings.applications.specialaccess.notificationaccess.BridgedAppsPreferenceController" />


    </PreferenceCategory>
</PreferenceScreen>
 No newline at end of file
+218 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.applications.specialaccess.notificationaccess;

import android.content.ComponentName;
import android.content.Context;
import android.content.pm.VersionedPackage;
import android.os.UserHandle;
import android.service.notification.NotificationListenerFilter;

import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;

import com.android.settings.applications.AppStateBaseBridge;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.notification.NotificationBackend;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
import com.android.settingslib.applications.ApplicationsState.AppFilter;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.core.lifecycle.LifecycleObserver;

import java.util.ArrayList;
import java.util.Set;
import java.util.TreeSet;


public class BridgedAppsPreferenceController extends BasePreferenceController implements
        LifecycleObserver, ApplicationsState.Callbacks,
        AppStateBaseBridge.Callback {

    private ApplicationsState mApplicationsState;
    private ApplicationsState.Session mSession;
    private AppFilter mFilter;
    private PreferenceScreen mScreen;

    private ComponentName mCn;
    private int mUserId;
    private NotificationBackend mNm;
    private NotificationListenerFilter mNlf;

    public BridgedAppsPreferenceController(Context context, String key) {
        super(context, key);
    }

    public BridgedAppsPreferenceController setAppState(ApplicationsState appState) {
        mApplicationsState = appState;
        return this;
    }

    public BridgedAppsPreferenceController setCn(ComponentName cn) {
        mCn = cn;
        return this;
    }

    public BridgedAppsPreferenceController setUserId(int userId) {
        mUserId = userId;
        return this;
    }

    public BridgedAppsPreferenceController setNm(NotificationBackend nm) {
        mNm = nm;
        return this;
    }

    public BridgedAppsPreferenceController setFilter(AppFilter filter) {
        mFilter = filter;
        return this;
    }

    public BridgedAppsPreferenceController setSession(Lifecycle lifecycle) {
        mSession = mApplicationsState.newSession(this, lifecycle);
        return this;
    }

    @Override
    public void displayPreference(PreferenceScreen screen) {
        mScreen = screen;
    }

    @Override
    public int getAvailabilityStatus() {
        return AVAILABLE;
    }


    @Override
    public void onExtraInfoUpdated() {
        rebuild();
    }

    @Override
    public void onRunningStateChanged(boolean running) {

    }

    @Override
    public void onPackageListChanged() {
        rebuild();
    }

    @Override
    public void onRebuildComplete(ArrayList<AppEntry> apps) {
        if (apps == null) {
            return;
        }
        mNlf = mNm.getListenerFilter(mCn, mUserId);

        // Create apps key set for removing useless preferences
        final Set<String> appsKeySet = new TreeSet<>();
        // Add or update preferences
        final int N = apps.size();
        for (int i = 0; i < N; i++) {
            final AppEntry entry = apps.get(i);
            if (!shouldAddPreference(entry)) {
                continue;
            }
            final String prefKey = entry.info.packageName + "|" + entry.info.uid;
            appsKeySet.add(prefKey);
            SwitchPreference preference = mScreen.findPreference(prefKey);
            if (preference == null) {
                preference = new SwitchPreference(mScreen.getContext());
                preference.setIcon(entry.icon);
                preference.setTitle(entry.label);
                preference.setKey(prefKey);
                mScreen.addPreference(preference);
            }
            preference.setOrder(i);
            preference.setChecked(mNlf.isPackageAllowed(
                    new VersionedPackage(entry.info.packageName, entry.info.uid)));
            preference.setOnPreferenceChangeListener(this::onPreferenceChange);
        }

        // Remove preferences that are no longer existing in the updated list of apps
        removeUselessPrefs(appsKeySet);
    }

    @Override
    public void onPackageIconChanged() {
        rebuild();
    }

    @Override
    public void onPackageSizeChanged(String packageName) {

    }

    @Override
    public void onAllSizesComputed() {
    }

    @Override
    public void onLauncherInfoChanged() {
    }

    @Override
    public void onLoadEntriesCompleted() {
        rebuild();
    }

    public boolean onPreferenceChange(Preference preference, Object newValue) {
        if (preference instanceof SwitchPreference) {
            String packageName = preference.getKey().substring(0, preference.getKey().indexOf("|"));
            int uid = Integer.parseInt(preference.getKey().substring(
                    preference.getKey().indexOf("|") + 1));
            boolean allowlisted = newValue == Boolean.TRUE;
            mNlf = mNm.getListenerFilter(mCn, mUserId);
            if (allowlisted) {
                mNlf.removePackage(new VersionedPackage(packageName, uid));
            } else {
                mNlf.addPackage(new VersionedPackage(packageName, uid));
            }
            mNm.setListenerFilter(mCn, mUserId, mNlf);
            return true;
        }
        return false;
    }

    public void rebuild() {
        final ArrayList<AppEntry> apps = mSession.rebuild(mFilter,
                ApplicationsState.ALPHA_COMPARATOR);
        if (apps != null) {
            onRebuildComplete(apps);
        }
    }

    private void removeUselessPrefs(final Set<String> appsKeySet) {
        final int prefCount = mScreen.getPreferenceCount();
        String prefKey;
        if (prefCount > 0) {
            for (int i = prefCount - 1; i >= 0; i--) {
                Preference pref = mScreen.getPreference(i);
                prefKey = pref.getKey();
                if (!appsKeySet.contains(prefKey)) {
                    mScreen.removePreference(pref);
                }
            }
        }
    }

    @VisibleForTesting
    static boolean shouldAddPreference(AppEntry app) {
        return app != null && UserHandle.isApp(app.info.uid);
    }
}
+137 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.applications.specialaccess.notificationaccess;

import static com.android.settings.applications.AppInfoBase.ARG_PACKAGE_NAME;

import android.app.Application;
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.UserHandle;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;

import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.notification.NotificationBackend;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState.AppFilter;

public class BridgedAppsSettings extends DashboardFragment {

    private static final String TAG = "BridgedAppsSettings";

    private static final int MENU_SHOW_SYSTEM = Menu.FIRST + 42;
    private static final String EXTRA_SHOW_SYSTEM = "show_system";

    private boolean mShowSystem;
    private AppFilter mFilter;

    private int mUserId;
    private ComponentName mComponentName;

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        mShowSystem = icicle != null && icicle.getBoolean(EXTRA_SHOW_SYSTEM);

        use(BridgedAppsPreferenceController.class).setNm(new NotificationBackend());
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        menu.add(Menu.NONE, MENU_SHOW_SYSTEM, Menu.NONE,
                mShowSystem ? R.string.menu_hide_system : R.string.menu_show_system);
        super.onCreateOptionsMenu(menu, inflater);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case MENU_SHOW_SYSTEM:
                mShowSystem = !mShowSystem;
                item.setTitle(mShowSystem ? R.string.menu_hide_system : R.string.menu_show_system);
                mFilter = mShowSystem ? ApplicationsState.FILTER_ALL_ENABLED
                        : ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER;

                use(BridgedAppsPreferenceController.class).setFilter(mFilter).rebuild();

                break;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean(EXTRA_SHOW_SYSTEM, mShowSystem);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        mFilter = mShowSystem ? ApplicationsState.FILTER_ALL_ENABLED
                : ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER;

        final Bundle args = getArguments();
        Intent intent = (args == null) ?
                getIntent() : (Intent) args.getParcelable("intent");
        String cn = args.getString(Settings.EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME);
        if (cn != null) {
            mComponentName = ComponentName.unflattenFromString(cn);
        }
        if (intent != null && intent.hasExtra(Intent.EXTRA_USER_HANDLE)) {
            mUserId = ((UserHandle) intent.getParcelableExtra(
                    Intent.EXTRA_USER_HANDLE)).getIdentifier();
        } else {
            mUserId = UserHandle.myUserId();
        }

        use(BridgedAppsPreferenceController.class)
                .setAppState(ApplicationsState.getInstance(
                        (Application) context.getApplicationContext()))
                .setCn(mComponentName)
                .setUserId(mUserId)
                .setSession(getSettingsLifecycle())
                .setFilter(mFilter)
                .rebuild();
    }

    @Override
    protected String getLogTag() {
        return TAG;
    }

    @Override
    public int getMetricsCategory() {
        return SettingsEnums.NOTIFICATION_ACCESS_BRIDGED_APPS;
    }

    @Override
    protected int getPreferenceScreenResId() {
        return R.xml.notification_access_bridged_apps_settings;
    }
}
Loading