1 /*
2  * Copyright (C) 2016 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 com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
20 
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.animation.ValueAnimator;
24 import android.app.Notification;
25 import android.app.NotificationChannel;
26 import android.app.NotificationChannelGroup;
27 import android.app.NotificationManager;
28 import android.content.BroadcastReceiver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.content.pm.ActivityInfo;
33 import android.content.pm.PackageInfo;
34 import android.content.pm.PackageManager;
35 import android.content.pm.PackageManager.NameNotFoundException;
36 import android.content.pm.ResolveInfo;
37 import android.content.pm.ShortcutInfo;
38 import android.graphics.drawable.Drawable;
39 import android.os.Bundle;
40 import android.os.UserHandle;
41 import android.provider.Settings;
42 import android.text.TextUtils;
43 import android.util.Log;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.view.ViewTreeObserver;
47 import android.view.animation.DecelerateInterpolator;
48 import android.widget.Toast;
49 
50 import androidx.annotation.NonNull;
51 import androidx.preference.PreferenceScreen;
52 
53 import com.android.settings.R;
54 import com.android.settings.SettingsActivity;
55 import com.android.settings.applications.AppInfoBase;
56 import com.android.settings.dashboard.DashboardFragment;
57 import com.android.settings.notification.NotificationBackend;
58 import com.android.settingslib.RestrictedLockUtilsInternal;
59 import com.android.settingslib.notification.ConversationIconFactory;
60 
61 import java.util.ArrayList;
62 import java.util.List;
63 
64 abstract public class NotificationSettings extends DashboardFragment {
65     private static final String TAG = "NotifiSettingsBase";
66     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
67 
68     protected PackageManager mPm;
69     protected NotificationBackend mBackend = new NotificationBackend();
70     protected NotificationManager mNm;
71     protected Context mContext;
72 
73     protected int mUid;
74     protected int mUserId;
75     protected String mPkg;
76     protected PackageInfo mPkgInfo;
77     protected EnforcedAdmin mSuspendedAppsAdmin;
78     protected NotificationChannelGroup mChannelGroup;
79     protected NotificationChannel mChannel;
80     protected NotificationBackend.AppRow mAppRow;
81     protected Drawable mConversationDrawable;
82     protected ShortcutInfo mConversationInfo;
83     protected List<String> mPreferenceFilter;
84 
85     protected boolean mShowLegacyChannelConfig = false;
86     protected boolean mListeningToPackageRemove;
87 
88     protected List<NotificationPreferenceController> mControllers = new ArrayList<>();
89     protected DependentFieldListener mDependentFieldListener = new DependentFieldListener();
90 
91     protected Intent mIntent;
92     protected Bundle mArgs;
93 
94     private ViewGroup mLayoutView;
95     private static final int DURATION_ANIMATE_PANEL_EXPAND_MS = 250;
96 
97     private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener =
98             new ViewTreeObserver.OnGlobalLayoutListener() {
99                 @Override
100                 public void onGlobalLayout() {
101                     animateIn();
102                     if (mLayoutView != null) {
103                         mLayoutView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
104                     }
105                 }
106             };
107 
108     @Override
onAttach(Context context)109     public void onAttach(Context context) {
110         super.onAttach(context);
111         mContext = getActivity();
112         mIntent = getActivity().getIntent();
113         mArgs = getArguments();
114 
115         mPm = getPackageManager();
116         mNm = NotificationManager.from(mContext);
117 
118         mPkg = mArgs != null && mArgs.containsKey(AppInfoBase.ARG_PACKAGE_NAME)
119                 ? mArgs.getString(AppInfoBase.ARG_PACKAGE_NAME)
120                 : mIntent.getStringExtra(Settings.EXTRA_APP_PACKAGE);
121         mUid = mArgs != null && mArgs.containsKey(AppInfoBase.ARG_PACKAGE_UID)
122                 ? mArgs.getInt(AppInfoBase.ARG_PACKAGE_UID)
123                 : mIntent.getIntExtra(Settings.EXTRA_APP_UID, -1);
124 
125         if (mUid < 0) {
126             try {
127                 mUid = mPm.getPackageUid(mPkg, 0);
128             } catch (NameNotFoundException e) {
129             }
130         }
131 
132         mPkgInfo = findPackageInfo(mPkg, mUid);
133 
134         if (mPkgInfo != null) {
135             mUserId = UserHandle.getUserId(mUid);
136             mSuspendedAppsAdmin = RestrictedLockUtilsInternal.checkIfApplicationIsSuspended(
137                     mContext, mPkg, mUserId);
138 
139             loadChannel();
140             loadAppRow();
141             loadChannelGroup();
142             loadPreferencesFilter();
143             collectConfigActivities();
144 
145             if (use(HeaderPreferenceController.class) != null) {
146                 getSettingsLifecycle().addObserver(use(HeaderPreferenceController.class));
147             }
148             if (use(ConversationHeaderPreferenceController.class) != null) {
149                 getSettingsLifecycle().addObserver(
150                         use(ConversationHeaderPreferenceController.class));
151             }
152 
153             for (NotificationPreferenceController controller : mControllers) {
154                 controller.onResume(mAppRow, mChannel, mChannelGroup, null, null,
155                         mSuspendedAppsAdmin, mPreferenceFilter);
156             }
157         }
158     }
159 
160     @Override
onCreate(Bundle savedInstanceState)161     public void onCreate(Bundle savedInstanceState) {
162         super.onCreate(savedInstanceState);
163 
164         if (mIntent == null && mArgs == null) {
165             Log.w(TAG, "No intent");
166             toastAndFinish();
167             return;
168         }
169 
170         if (mUid < 0 || TextUtils.isEmpty(mPkg) || mPkgInfo == null) {
171             Log.w(TAG, "Missing package or uid or packageinfo");
172             toastAndFinish();
173             return;
174         }
175 
176         startListeningToPackageRemove();
177     }
178 
179     @Override
onDestroy()180     public void onDestroy() {
181         stopListeningToPackageRemove();
182         super.onDestroy();
183     }
184 
185     @Override
onResume()186     public void onResume() {
187         super.onResume();
188         if (mUid < 0 || TextUtils.isEmpty(mPkg) || mPkgInfo == null || mAppRow == null) {
189             Log.w(TAG, "Missing package or uid or packageinfo");
190             finish();
191             return;
192         }
193         // Reload app, channel, etc onResume in case they've changed. A little wasteful if we've
194         // just done onAttach but better than making every preference controller reload all
195         // the data
196         loadAppRow();
197         if (mAppRow == null) {
198             Log.w(TAG, "Can't load package");
199             finish();
200             return;
201         }
202         loadChannel();
203         loadConversation();
204         loadChannelGroup();
205         loadPreferencesFilter();
206         collectConfigActivities();
207     }
208 
animatePanel()209     protected void animatePanel() {
210         if (mPreferenceFilter != null) {
211             mLayoutView = getActivity().findViewById(R.id.main_content);
212             mLayoutView.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
213         }
214     }
215 
216     /**
217      * Animate a Panel onto the screen.
218      * <p>
219      * Takes the entire panel and animates in from behind the navigation bar.
220      * <p>
221      * Relies on the Panel being having a fixed height to begin the animation.
222      */
animateIn()223     private void animateIn() {
224         final AnimatorSet animatorSet = buildAnimatorSet(mLayoutView,
225                 mLayoutView.getHeight() /* startY */, 0.0f /* endY */,
226                 0.0f /* startAlpha */, 1.0f /* endAlpha */,
227                 DURATION_ANIMATE_PANEL_EXPAND_MS);
228         final ValueAnimator animator = new ValueAnimator();
229         animator.setFloatValues(0.0f, 1.0f);
230         animatorSet.play(animator);
231         animatorSet.start();
232     }
233 
234     /**
235      * Build an {@link AnimatorSet} to animate the Panel, {@param parentView} in or out of the
236      * screen, based on the positional parameters {@param startY}, {@param endY}, the parameters
237      * for alpha changes {@param startAlpha}, {@param endAlpha}, and the {@param duration} in
238      * milliseconds.
239      */
240     @NonNull
buildAnimatorSet(@onNull View targetView, float startY, float endY, float startAlpha, float endAlpha, int duration)241     private static AnimatorSet buildAnimatorSet(@NonNull View targetView,
242             float startY, float endY,
243             float startAlpha, float endAlpha, int duration) {
244         final AnimatorSet animatorSet = new AnimatorSet();
245         animatorSet.setDuration(duration);
246         animatorSet.setInterpolator(new DecelerateInterpolator());
247         animatorSet.playTogether(
248                 ObjectAnimator.ofFloat(targetView, View.TRANSLATION_Y, startY, endY),
249                 ObjectAnimator.ofFloat(targetView, View.ALPHA, startAlpha, endAlpha));
250         return animatorSet;
251     }
252 
loadPreferencesFilter()253     private void loadPreferencesFilter() {
254         Intent intent = getActivity().getIntent();
255         mPreferenceFilter = intent != null
256                 ? intent.getStringArrayListExtra(Settings.EXTRA_CHANNEL_FILTER_LIST)
257                 : null;
258     }
259 
loadChannel()260     private void loadChannel() {
261         Intent intent = getActivity().getIntent();
262         String channelId = intent != null ? intent.getStringExtra(Settings.EXTRA_CHANNEL_ID) : null;
263         if (channelId == null && intent != null) {
264             Bundle args = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
265             channelId = args != null ? args.getString(Settings.EXTRA_CHANNEL_ID) : null;
266         }
267         String conversationId = intent != null
268                 ? intent.getStringExtra(Settings.EXTRA_CONVERSATION_ID) : null;
269         mChannel = mBackend.getChannel(mPkg, mUid, channelId, conversationId);
270         if (mChannel == null) {
271             mBackend.getChannel(mPkg, mUid, channelId, null);
272         }
273     }
274 
loadConversation()275     private void loadConversation() {
276         if (mChannel == null || TextUtils.isEmpty(mChannel.getConversationId())
277                 || mChannel.isDemoted()) {
278             return;
279         }
280         mConversationInfo = mBackend.getConversationInfo(
281                 mContext, mPkg, mUid, mChannel.getConversationId());
282         if (mConversationInfo != null) {
283             mConversationDrawable = mBackend.getConversationDrawable(
284                     mContext, mConversationInfo, mAppRow.pkg, mAppRow.uid,
285                     mChannel.isImportantConversation());
286         }
287     }
288 
loadAppRow()289     private void loadAppRow() {
290         mAppRow = mBackend.loadAppRow(mContext, mPm, mPkgInfo);
291     }
292 
loadChannelGroup()293     private void loadChannelGroup() {
294         mShowLegacyChannelConfig = mBackend.onlyHasDefaultChannel(mAppRow.pkg, mAppRow.uid)
295                 || (mChannel != null
296                 && NotificationChannel.DEFAULT_CHANNEL_ID.equals(mChannel.getId()));
297 
298         if (mShowLegacyChannelConfig) {
299             mChannel = mBackend.getChannel(
300                     mAppRow.pkg, mAppRow.uid, NotificationChannel.DEFAULT_CHANNEL_ID, null);
301         }
302         if (mChannel != null && !TextUtils.isEmpty(mChannel.getGroup())) {
303             NotificationChannelGroup group = mBackend.getGroup(mPkg, mUid, mChannel.getGroup());
304             if (group != null) {
305                 mChannelGroup = group;
306             }
307         }
308     }
309 
toastAndFinish()310     protected void toastAndFinish() {
311         Toast.makeText(mContext, R.string.app_not_found_dlg_text, Toast.LENGTH_SHORT).show();
312         getActivity().finish();
313     }
314 
collectConfigActivities()315     protected void collectConfigActivities() {
316         Intent intent = new Intent(Intent.ACTION_MAIN)
317                 .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
318                 .setPackage(mAppRow.pkg);
319         final List<ResolveInfo> resolveInfos = mPm.queryIntentActivities(
320                 intent,
321                 0 //PackageManager.MATCH_DEFAULT_ONLY
322         );
323         if (DEBUG) {
324             Log.d(TAG, "Found " + resolveInfos.size() + " preference activities"
325                     + (resolveInfos.size() == 0 ? " ;_;" : ""));
326         }
327         for (ResolveInfo ri : resolveInfos) {
328             final ActivityInfo activityInfo = ri.activityInfo;
329             if (mAppRow.settingsIntent != null) {
330                 if (DEBUG) {
331                     Log.d(TAG, "Ignoring duplicate notification preference activity ("
332                             + activityInfo.name + ") for package "
333                             + activityInfo.packageName);
334                 }
335                 continue;
336             }
337             mAppRow.settingsIntent = intent
338                     .setPackage(null)
339                     .setClassName(activityInfo.packageName, activityInfo.name)
340                     .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
341             if (mChannel != null) {
342                 mAppRow.settingsIntent.putExtra(Notification.EXTRA_CHANNEL_ID, mChannel.getId());
343             }
344             if (mChannelGroup != null) {
345                 mAppRow.settingsIntent.putExtra(
346                         Notification.EXTRA_CHANNEL_GROUP_ID, mChannelGroup.getId());
347             }
348         }
349     }
350 
findPackageInfo(String pkg, int uid)351     private PackageInfo findPackageInfo(String pkg, int uid) {
352         if (pkg == null || uid < 0) {
353             return null;
354         }
355         final String[] packages = mPm.getPackagesForUid(uid);
356         if (packages != null && pkg != null) {
357             final int N = packages.length;
358             for (int i = 0; i < N; i++) {
359                 final String p = packages[i];
360                 if (pkg.equals(p)) {
361                     try {
362                         return mPm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES
363                                 | PackageManager.GET_PERMISSIONS);
364                     } catch (NameNotFoundException e) {
365                         Log.w(TAG, "Failed to load package " + pkg, e);
366                     }
367                 }
368             }
369         }
370         return null;
371     }
372 
startListeningToPackageRemove()373     protected void startListeningToPackageRemove() {
374         if (mListeningToPackageRemove) {
375             return;
376         }
377         mListeningToPackageRemove = true;
378         final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
379         filter.addDataScheme("package");
380         getContext().registerReceiver(mPackageRemovedReceiver, filter);
381     }
382 
stopListeningToPackageRemove()383     protected void stopListeningToPackageRemove() {
384         if (!mListeningToPackageRemove) {
385             return;
386         }
387         mListeningToPackageRemove = false;
388         getContext().unregisterReceiver(mPackageRemovedReceiver);
389     }
390 
onPackageRemoved()391     protected void onPackageRemoved() {
392         getActivity().finishAndRemoveTask();
393     }
394 
395     protected final BroadcastReceiver mPackageRemovedReceiver = new BroadcastReceiver() {
396         @Override
397         public void onReceive(Context context, Intent intent) {
398             String packageName = intent.getData().getSchemeSpecificPart();
399             if (mPkgInfo == null || TextUtils.equals(mPkgInfo.packageName, packageName)) {
400                 if (DEBUG) {
401                     Log.d(TAG, "Package (" + packageName + ") removed. Removing"
402                             + "NotificationSettingsBase.");
403                 }
404                 onPackageRemoved();
405             }
406         }
407     };
408 
409     protected class DependentFieldListener {
onFieldValueChanged()410         protected void onFieldValueChanged() {
411             // Reload the conversation drawable, which shows some channel/conversation state
412             if (mConversationDrawable != null && mConversationDrawable
413                     instanceof ConversationIconFactory.ConversationIconDrawable) {
414                 ((ConversationIconFactory.ConversationIconDrawable) mConversationDrawable)
415                         .setImportant(mChannel.isImportantConversation());
416             }
417             final PreferenceScreen screen = getPreferenceScreen();
418             for (NotificationPreferenceController controller : mControllers) {
419                 controller.displayPreference(screen);
420             }
421             updatePreferenceStates();
422         }
423     }
424 }
425