1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.notification.app;
18 
19 import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE;
20 import static android.provider.Settings.Secure.NOTIFICATION_BUBBLES;
21 
22 import android.annotation.Nullable;
23 import android.app.ActivityManager;
24 import android.app.NotificationChannel;
25 import android.content.Context;
26 import android.provider.Settings;
27 
28 import androidx.annotation.VisibleForTesting;
29 import androidx.fragment.app.FragmentManager;
30 import androidx.preference.Preference;
31 
32 import com.android.settings.core.PreferenceControllerMixin;
33 import com.android.settings.notification.NotificationBackend;
34 import com.android.settingslib.RestrictedSwitchPreference;
35 
36 /**
37  * Preference controller for Bubbles. This is used as the app-specific page and conversation
38  * settings.
39  */
40 public class BubblePreferenceController extends NotificationPreferenceController
41         implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener {
42 
43     private static final String TAG = "BubblePrefContr";
44     private static final String KEY = "bubble_pref";
45     @VisibleForTesting
46     static final int SYSTEM_WIDE_ON = 1;
47     @VisibleForTesting
48     static final int SYSTEM_WIDE_OFF = 0;
49 
50     private FragmentManager mFragmentManager;
51     private boolean mIsAppPage;
52     private boolean mHasSentInvalidMsg;
53     private int mNumConversations;
54     private NotificationSettings.DependentFieldListener mListener;
55 
BubblePreferenceController(Context context, @Nullable FragmentManager fragmentManager, NotificationBackend backend, boolean isAppPage, @Nullable NotificationSettings.DependentFieldListener listener)56     public BubblePreferenceController(Context context, @Nullable FragmentManager fragmentManager,
57             NotificationBackend backend, boolean isAppPage,
58             @Nullable NotificationSettings.DependentFieldListener listener) {
59         super(context, backend);
60         mFragmentManager = fragmentManager;
61         mIsAppPage = isAppPage;
62         mListener = listener;
63     }
64 
65     @Override
getPreferenceKey()66     public String getPreferenceKey() {
67         return KEY;
68     }
69 
70     @Override
isAvailable()71     public boolean isAvailable() {
72         if (!super.isAvailable()) {
73             return false;
74         }
75         if (!mIsAppPage && !isEnabled()) {
76             return false;
77         }
78         if (mChannel != null) {
79             if (isDefaultChannel()) {
80                 return true;
81             } else {
82                 return mAppRow != null &&  mAppRow.bubblePreference != BUBBLE_PREFERENCE_NONE;
83             }
84         }
85         return true;
86     }
87 
88     @Override
isIncludedInFilter()89     boolean isIncludedInFilter() {
90         return mPreferenceFilter.contains(NotificationChannel.EDIT_CONVERSATION);
91     }
92 
93     @Override
updateState(Preference preference)94     public void updateState(Preference preference) {
95         if (mIsAppPage && mAppRow != null) {
96             mHasSentInvalidMsg = mBackend.isInInvalidMsgState(mAppRow.pkg, mAppRow.uid);
97             mNumConversations = mBackend.getConversations(
98                     mAppRow.pkg, mAppRow.uid).getList().size();
99             // We're on the app specific bubble page which displays a tri-state
100             int backEndPref = mAppRow.bubblePreference;
101             BubblePreference pref = (BubblePreference) preference;
102             pref.setDisabledByAdmin(mAdmin);
103             pref.setSelectedVisibility(!mHasSentInvalidMsg || mNumConversations > 0);
104             if (!isEnabled()) {
105                 pref.setSelectedPreference(BUBBLE_PREFERENCE_NONE);
106             } else {
107                 pref.setSelectedPreference(backEndPref);
108             }
109         } else if (mChannel != null) {
110             // We're on the channel specific notification page which displays a toggle.
111             RestrictedSwitchPreference switchpref = (RestrictedSwitchPreference) preference;
112             switchpref.setDisabledByAdmin(mAdmin);
113             switchpref.setChecked(mChannel.canBubble() && isEnabled());
114         }
115     }
116 
117     @Override
onPreferenceChange(Preference preference, Object newValue)118     public boolean onPreferenceChange(Preference preference, Object newValue) {
119         if (mChannel != null) {
120             // Channel page is toggle
121             mChannel.setAllowBubbles((boolean) newValue);
122             saveChannel();
123         } else if (mIsAppPage) {
124             // App page is bubble preference
125             BubblePreference pref = (BubblePreference) preference;
126             if (mAppRow != null && mFragmentManager != null) {
127                 final int value = (int) newValue;
128                 if (!isEnabled()
129                         && pref.getSelectedPreference() == BUBBLE_PREFERENCE_NONE) {
130                     // if the global setting is off, toggling app level permission requires extra
131                     // confirmation
132                     new BubbleWarningDialogFragment()
133                             .setPkgPrefInfo(mAppRow.pkg, mAppRow.uid, value)
134                             .show(mFragmentManager, "dialog");
135                     return false;
136                 } else {
137                     mAppRow.bubblePreference = value;
138                     mBackend.setAllowBubbles(mAppRow.pkg, mAppRow.uid, value);
139                 }
140             }
141             if (mListener != null) {
142                 mListener.onFieldValueChanged();
143             }
144         }
145         return true;
146     }
147 
isEnabled()148     private boolean isEnabled() {
149         ActivityManager am = mContext.getSystemService(ActivityManager.class);
150         return !am.isLowRamDevice() && Settings.Secure.getInt(mContext.getContentResolver(),
151                 NOTIFICATION_BUBBLES, SYSTEM_WIDE_OFF) == SYSTEM_WIDE_ON;
152     }
153 
154     /**
155      * Used in app level prompt that confirms the user is ok with turning on bubbles
156      * globally. If they aren't, undo that.
157      */
revertBubblesApproval(Context context, String pkg, int uid)158     public static void revertBubblesApproval(Context context, String pkg, int uid) {
159         NotificationBackend backend = new NotificationBackend();
160         backend.setAllowBubbles(pkg, uid, BUBBLE_PREFERENCE_NONE);
161 
162         // changing the global settings will cause the observer on the host page to reload
163         // correct preference state
164         Settings.Secure.putInt(context.getContentResolver(),
165                 NOTIFICATION_BUBBLES,
166                 SYSTEM_WIDE_OFF);
167     }
168 
169     /**
170      * Apply global bubbles approval
171      */
applyBubblesApproval(Context context, String pkg, int uid, int pref)172     public static void applyBubblesApproval(Context context, String pkg, int uid, int pref) {
173         NotificationBackend backend = new NotificationBackend();
174         backend.setAllowBubbles(pkg, uid, pref);
175         // changing the global settings will cause the observer on the host page to reload
176         // correct preference state
177         Settings.Secure.putInt(context.getContentResolver(),
178                 NOTIFICATION_BUBBLES,
179                 SYSTEM_WIDE_ON);
180     }
181 }
182