1 /*
2  * Copyright (C) 2017 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.systemui.statusbar;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.os.Handler;
22 import android.os.Trace;
23 import android.os.UserHandle;
24 import android.util.Log;
25 import android.view.View;
26 import android.view.ViewGroup;
27 
28 import com.android.systemui.R;
29 import com.android.systemui.dagger.qualifiers.Main;
30 import com.android.systemui.flags.FeatureFlags;
31 import com.android.systemui.plugins.statusbar.StatusBarStateController;
32 import com.android.systemui.statusbar.dagger.StatusBarModule;
33 import com.android.systemui.statusbar.notification.AssistantFeedbackController;
34 import com.android.systemui.statusbar.notification.DynamicChildBindController;
35 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
36 import com.android.systemui.statusbar.notification.NotificationEntryManager;
37 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
38 import com.android.systemui.statusbar.notification.collection.inflation.LowPriorityInflationHelper;
39 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
40 import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
41 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
42 import com.android.systemui.statusbar.notification.stack.ForegroundServiceSectionController;
43 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
44 import com.android.systemui.statusbar.phone.KeyguardBypassController;
45 import com.android.systemui.util.Assert;
46 import com.android.wm.shell.bubbles.Bubbles;
47 
48 import java.util.ArrayList;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.Optional;
52 import java.util.Stack;
53 
54 /**
55  * NotificationViewHierarchyManager manages updating the view hierarchy of notification views based
56  * on their group structure. For example, if a notification becomes bundled with another,
57  * NotificationViewHierarchyManager will update the view hierarchy to reflect that. It also will
58  * tell NotificationListContainer which notifications to display, and inform it of changes to those
59  * notifications that might affect their display.
60  */
61 public class NotificationViewHierarchyManager implements DynamicPrivacyController.Listener {
62     private static final String TAG = "NotificationViewHierarchyManager";
63 
64     private final Handler mHandler;
65 
66     /**
67      * Re-usable map of top-level notifications to their sorted children if any.
68      * If the top-level notification doesn't have children, its key will still exist in this map
69      * with its value explicitly set to null.
70      */
71     private final HashMap<NotificationEntry, List<NotificationEntry>> mTmpChildOrderMap =
72             new HashMap<>();
73 
74     // Dependencies:
75     private final DynamicChildBindController mDynamicChildBindController;
76     private final FeatureFlags mFeatureFlags;
77     protected final NotificationLockscreenUserManager mLockscreenUserManager;
78     protected final NotificationGroupManagerLegacy mGroupManager;
79     protected final VisualStabilityManager mVisualStabilityManager;
80     private final SysuiStatusBarStateController mStatusBarStateController;
81     private final NotificationEntryManager mEntryManager;
82     private final LowPriorityInflationHelper mLowPriorityInflationHelper;
83 
84     /**
85      * {@code true} if notifications not part of a group should by default be rendered in their
86      * expanded state. If {@code false}, then only the first notification will be expanded if
87      * possible.
88      */
89     private final boolean mAlwaysExpandNonGroupedNotification;
90     private final Optional<Bubbles> mBubblesOptional;
91     private final DynamicPrivacyController mDynamicPrivacyController;
92     private final KeyguardBypassController mBypassController;
93     private final ForegroundServiceSectionController mFgsSectionController;
94     private AssistantFeedbackController mAssistantFeedbackController;
95     private final Context mContext;
96 
97     private NotificationPresenter mPresenter;
98     private NotificationListContainer mListContainer;
99 
100     // Used to help track down re-entrant calls to our update methods, which will cause bugs.
101     private boolean mPerformingUpdate;
102     // Hack to get around re-entrant call in onDynamicPrivacyChanged() until we can track down
103     // the problem.
104     private boolean mIsHandleDynamicPrivacyChangeScheduled;
105 
106     /**
107      * Injected constructor. See {@link StatusBarModule}.
108      */
NotificationViewHierarchyManager( Context context, @Main Handler mainHandler, FeatureFlags featureFlags, NotificationLockscreenUserManager notificationLockscreenUserManager, NotificationGroupManagerLegacy groupManager, VisualStabilityManager visualStabilityManager, StatusBarStateController statusBarStateController, NotificationEntryManager notificationEntryManager, KeyguardBypassController bypassController, Optional<Bubbles> bubblesOptional, DynamicPrivacyController privacyController, ForegroundServiceSectionController fgsSectionController, DynamicChildBindController dynamicChildBindController, LowPriorityInflationHelper lowPriorityInflationHelper, AssistantFeedbackController assistantFeedbackController)109     public NotificationViewHierarchyManager(
110             Context context,
111             @Main Handler mainHandler,
112             FeatureFlags featureFlags,
113             NotificationLockscreenUserManager notificationLockscreenUserManager,
114             NotificationGroupManagerLegacy groupManager,
115             VisualStabilityManager visualStabilityManager,
116             StatusBarStateController statusBarStateController,
117             NotificationEntryManager notificationEntryManager,
118             KeyguardBypassController bypassController,
119             Optional<Bubbles> bubblesOptional,
120             DynamicPrivacyController privacyController,
121             ForegroundServiceSectionController fgsSectionController,
122             DynamicChildBindController dynamicChildBindController,
123             LowPriorityInflationHelper lowPriorityInflationHelper,
124             AssistantFeedbackController assistantFeedbackController) {
125         mContext = context;
126         mHandler = mainHandler;
127         mFeatureFlags = featureFlags;
128         mLockscreenUserManager = notificationLockscreenUserManager;
129         mBypassController = bypassController;
130         mGroupManager = groupManager;
131         mVisualStabilityManager = visualStabilityManager;
132         mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController;
133         mEntryManager = notificationEntryManager;
134         mFgsSectionController = fgsSectionController;
135         Resources res = context.getResources();
136         mAlwaysExpandNonGroupedNotification =
137                 res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications);
138         mBubblesOptional = bubblesOptional;
139         mDynamicPrivacyController = privacyController;
140         mDynamicChildBindController = dynamicChildBindController;
141         mLowPriorityInflationHelper = lowPriorityInflationHelper;
142         mAssistantFeedbackController = assistantFeedbackController;
143     }
144 
setUpWithPresenter(NotificationPresenter presenter, NotificationListContainer listContainer)145     public void setUpWithPresenter(NotificationPresenter presenter,
146             NotificationListContainer listContainer) {
147         mPresenter = presenter;
148         mListContainer = listContainer;
149         if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
150             mDynamicPrivacyController.addListener(this);
151         }
152     }
153 
154     /**
155      * Updates the visual representation of the notifications.
156      */
157     //TODO: Rewrite this to focus on Entries, or some other data object instead of views
updateNotificationViews()158     public void updateNotificationViews() {
159         Assert.isMainThread();
160         if (!mFeatureFlags.checkLegacyPipelineEnabled()) {
161             return;
162         }
163 
164         beginUpdate();
165 
166         List<NotificationEntry> activeNotifications = mEntryManager.getVisibleNotifications();
167         ArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size());
168         final int N = activeNotifications.size();
169         for (int i = 0; i < N; i++) {
170             NotificationEntry ent = activeNotifications.get(i);
171             if (shouldSuppressActiveNotification(ent)) {
172                 continue;
173             }
174 
175             int userId = ent.getSbn().getUserId();
176 
177             // Display public version of the notification if we need to redact.
178             // TODO: This area uses a lot of calls into NotificationLockscreenUserManager.
179             // We can probably move some of this code there.
180             int currentUserId = mLockscreenUserManager.getCurrentUserId();
181             boolean devicePublic = mLockscreenUserManager.isLockscreenPublicMode(currentUserId);
182             boolean userPublic = devicePublic
183                     || mLockscreenUserManager.isLockscreenPublicMode(userId);
184             if (userPublic && mDynamicPrivacyController.isDynamicallyUnlocked()
185                     && (userId == currentUserId || userId == UserHandle.USER_ALL
186                     || !mLockscreenUserManager.needsSeparateWorkChallenge(userId))) {
187                 userPublic = false;
188             }
189             boolean needsRedaction = mLockscreenUserManager.needsRedaction(ent);
190             boolean sensitive = userPublic && needsRedaction;
191             boolean deviceSensitive = devicePublic
192                     && !mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(
193                     currentUserId);
194             ent.setSensitive(sensitive, deviceSensitive);
195             ent.getRow().setNeedsRedaction(needsRedaction);
196             mLowPriorityInflationHelper.recheckLowPriorityViewAndInflate(ent, ent.getRow());
197             boolean isChildInGroup = mGroupManager.isChildInGroup(ent);
198 
199             boolean groupChangesAllowed =
200                     mVisualStabilityManager.areGroupChangesAllowed() // user isn't looking at notifs
201                     || !ent.hasFinishedInitialization(); // notif recently added
202 
203             NotificationEntry parent = mGroupManager.getGroupSummary(ent);
204             if (!groupChangesAllowed) {
205                 // We don't to change groups while the user is looking at them
206                 boolean wasChildInGroup = ent.isChildInGroup();
207                 if (isChildInGroup && !wasChildInGroup) {
208                     isChildInGroup = wasChildInGroup;
209                     mVisualStabilityManager.addGroupChangesAllowedCallback(mEntryManager,
210                             false /* persistent */);
211                 } else if (!isChildInGroup && wasChildInGroup) {
212                     // We allow grouping changes if the group was collapsed
213                     if (mGroupManager.isLogicalGroupExpanded(ent.getSbn())) {
214                         isChildInGroup = wasChildInGroup;
215                         parent = ent.getRow().getNotificationParent().getEntry();
216                         mVisualStabilityManager.addGroupChangesAllowedCallback(mEntryManager,
217                                 false /* persistent */);
218                     }
219                 }
220             }
221 
222             if (isChildInGroup) {
223                 List<NotificationEntry> orderedChildren = mTmpChildOrderMap.get(parent);
224                 if (orderedChildren == null) {
225                     orderedChildren = new ArrayList<>();
226                     mTmpChildOrderMap.put(parent, orderedChildren);
227                 }
228                 orderedChildren.add(ent);
229             } else {
230                 // Top-level notif (either a summary or single notification)
231 
232                 // A child may have already added its summary to mTmpChildOrderMap with a
233                 // list of children. This can happen since there's no guarantee summaries are
234                 // sorted before its children.
235                 if (!mTmpChildOrderMap.containsKey(ent)) {
236                     // mTmpChildOrderMap's keyset is used to iterate through all entries, so it's
237                     // necessary to add each top-level notif as a key
238                     mTmpChildOrderMap.put(ent, null);
239                 }
240                 toShow.add(ent.getRow());
241             }
242 
243         }
244 
245         ArrayList<ExpandableNotificationRow> viewsToRemove = new ArrayList<>();
246         for (int i=0; i< mListContainer.getContainerChildCount(); i++) {
247             View child = mListContainer.getContainerChildAt(i);
248             if (!toShow.contains(child) && child instanceof ExpandableNotificationRow) {
249                 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
250 
251                 // Blocking helper is effectively a detached view. Don't bother removing it from the
252                 // layout.
253                 if (!row.isBlockingHelperShowing()) {
254                     viewsToRemove.add((ExpandableNotificationRow) child);
255                 }
256             }
257         }
258 
259         for (ExpandableNotificationRow viewToRemove : viewsToRemove) {
260             NotificationEntry entry = viewToRemove.getEntry();
261             if (mEntryManager.getPendingOrActiveNotif(entry.getKey()) != null
262                 && !shouldSuppressActiveNotification(entry)) {
263                 // we are only transferring this notification to its parent, don't generate an
264                 // animation. If the notification is suppressed, this isn't a transfer.
265                 mListContainer.setChildTransferInProgress(true);
266             }
267             if (viewToRemove.isSummaryWithChildren()) {
268                 viewToRemove.removeAllChildren();
269             }
270             mListContainer.removeContainerView(viewToRemove);
271             mListContainer.setChildTransferInProgress(false);
272         }
273 
274         removeNotificationChildren();
275 
276         for (int i = 0; i < toShow.size(); i++) {
277             View v = toShow.get(i);
278             if (v.getParent() == null) {
279                 mVisualStabilityManager.notifyViewAddition(v);
280                 mListContainer.addContainerView(v);
281             } else if (!mListContainer.containsView(v)) {
282                 // the view is added somewhere else. Let's make sure
283                 // the ordering works properly below, by excluding these
284                 toShow.remove(v);
285                 i--;
286             }
287         }
288 
289         addNotificationChildrenAndSort();
290 
291         // So after all this work notifications still aren't sorted correctly.
292         // Let's do that now by advancing through toShow and mListContainer in
293         // lock-step, making sure mListContainer matches what we see in toShow.
294         int j = 0;
295         for (int i = 0; i < mListContainer.getContainerChildCount(); i++) {
296             View child = mListContainer.getContainerChildAt(i);
297             if (!(child instanceof ExpandableNotificationRow)) {
298                 // We don't care about non-notification views.
299                 continue;
300             }
301             if (((ExpandableNotificationRow) child).isBlockingHelperShowing()) {
302                 // Don't count/reorder notifications that are showing the blocking helper!
303                 continue;
304             }
305 
306             ExpandableNotificationRow targetChild = toShow.get(j);
307             if (child != targetChild) {
308                 // Oops, wrong notification at this position. Put the right one
309                 // here and advance both lists.
310                 if (mVisualStabilityManager.canReorderNotification(targetChild)) {
311                     mListContainer.changeViewPosition(targetChild, i);
312                 } else {
313                     mVisualStabilityManager.addReorderingAllowedCallback(mEntryManager,
314                             false  /* persistent */);
315                 }
316             }
317             j++;
318 
319         }
320 
321         mDynamicChildBindController.updateContentViews(mTmpChildOrderMap);
322         mVisualStabilityManager.onReorderingFinished();
323         // clear the map again for the next usage
324         mTmpChildOrderMap.clear();
325 
326         updateRowStatesInternal();
327 
328         mListContainer.onNotificationViewUpdateFinished();
329 
330         endUpdate();
331     }
332 
333     /**
334      * Should a notification entry from the active list be suppressed and not show?
335      */
shouldSuppressActiveNotification(NotificationEntry ent)336     private boolean shouldSuppressActiveNotification(NotificationEntry ent) {
337         final boolean isBubbleNotificationSuppressedFromShade = mBubblesOptional.isPresent()
338                 && mBubblesOptional.get().isBubbleNotificationSuppressedFromShade(
339                         ent.getKey(), ent.getSbn().getGroupKey());
340         if (ent.isRowDismissed() || ent.isRowRemoved()
341                 || isBubbleNotificationSuppressedFromShade
342                 || mFgsSectionController.hasEntry(ent)) {
343             // we want to suppress removed notifications because they could
344             // temporarily become children if they were isolated before.
345             return true;
346         }
347         return false;
348     }
349 
addNotificationChildrenAndSort()350     private void addNotificationChildrenAndSort() {
351         // Let's now add all notification children which are missing
352         boolean orderChanged = false;
353         ArrayList<ExpandableNotificationRow> orderedRows = new ArrayList<>();
354         for (int i = 0; i < mListContainer.getContainerChildCount(); i++) {
355             View view = mListContainer.getContainerChildAt(i);
356             if (!(view instanceof ExpandableNotificationRow)) {
357                 // We don't care about non-notification views.
358                 continue;
359             }
360 
361             ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
362             List<ExpandableNotificationRow> children = parent.getAttachedChildren();
363             List<NotificationEntry> orderedChildren = mTmpChildOrderMap.get(parent.getEntry());
364             if (orderedChildren == null) {
365                 // Not a group
366                 continue;
367             }
368             parent.setUntruncatedChildCount(orderedChildren.size());
369             for (int childIndex = 0; childIndex < orderedChildren.size(); childIndex++) {
370                 ExpandableNotificationRow childView = orderedChildren.get(childIndex).getRow();
371                 if (children == null || !children.contains(childView)) {
372                     if (childView.getParent() != null) {
373                         Log.wtf(TAG, "trying to add a notification child that already has "
374                                 + "a parent. class:" + childView.getParent().getClass()
375                                 + "\n child: " + childView);
376                         // This shouldn't happen. We can recover by removing it though.
377                         ((ViewGroup) childView.getParent()).removeView(childView);
378                     }
379                     mVisualStabilityManager.notifyViewAddition(childView);
380                     parent.addChildNotification(childView, childIndex);
381                     mListContainer.notifyGroupChildAdded(childView);
382                 }
383                 orderedRows.add(childView);
384             }
385 
386             // Finally after removing and adding has been performed we can apply the order.
387             orderChanged |= parent.applyChildOrder(orderedRows, mVisualStabilityManager,
388                     mEntryManager);
389             orderedRows.clear();
390         }
391         if (orderChanged) {
392             mListContainer.generateChildOrderChangedEvent();
393         }
394     }
395 
removeNotificationChildren()396     private void removeNotificationChildren() {
397         // First let's remove all children which don't belong in the parents
398         ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>();
399         for (int i = 0; i < mListContainer.getContainerChildCount(); i++) {
400             View view = mListContainer.getContainerChildAt(i);
401             if (!(view instanceof ExpandableNotificationRow)) {
402                 // We don't care about non-notification views.
403                 continue;
404             }
405 
406             ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
407             List<ExpandableNotificationRow> children = parent.getAttachedChildren();
408             List<NotificationEntry> orderedChildren = mTmpChildOrderMap.get(parent.getEntry());
409 
410             if (children != null) {
411                 toRemove.clear();
412                 for (ExpandableNotificationRow childRow : children) {
413                     if ((orderedChildren == null
414                             || !orderedChildren.contains(childRow.getEntry()))
415                             && !childRow.keepInParent()) {
416                         toRemove.add(childRow);
417                     }
418                 }
419                 for (ExpandableNotificationRow remove : toRemove) {
420                     parent.removeChildNotification(remove);
421                     if (mEntryManager.getActiveNotificationUnfiltered(
422                             remove.getEntry().getSbn().getKey()) == null) {
423                         // We only want to add an animation if the view is completely removed
424                         // otherwise it's just a transfer
425                         mListContainer.notifyGroupChildRemoved(remove,
426                                 parent.getChildrenContainer());
427                     }
428                 }
429             }
430         }
431     }
432 
433     /**
434      * Updates expanded, dimmed and locked states of notification rows.
435      */
updateRowStates()436     public void updateRowStates() {
437         Assert.isMainThread();
438         if (!mFeatureFlags.checkLegacyPipelineEnabled()) {
439             return;
440         }
441 
442         beginUpdate();
443         updateRowStatesInternal();
444         endUpdate();
445     }
446 
updateRowStatesInternal()447     private void updateRowStatesInternal() {
448         Trace.beginSection("NotificationViewHierarchyManager#updateRowStates");
449         final int N = mListContainer.getContainerChildCount();
450 
451         int visibleNotifications = 0;
452         boolean onKeyguard =
453                 mStatusBarStateController.getCurrentOrUpcomingState() == StatusBarState.KEYGUARD;
454         Stack<ExpandableNotificationRow> stack = new Stack<>();
455         for (int i = N - 1; i >= 0; i--) {
456             View child = mListContainer.getContainerChildAt(i);
457             if (!(child instanceof ExpandableNotificationRow)) {
458                 continue;
459             }
460             stack.push((ExpandableNotificationRow) child);
461         }
462         while(!stack.isEmpty()) {
463             ExpandableNotificationRow row = stack.pop();
464             NotificationEntry entry = row.getEntry();
465             boolean isChildNotification = mGroupManager.isChildInGroup(entry);
466 
467             if (!onKeyguard) {
468                 // If mAlwaysExpandNonGroupedNotification is false, then only expand the
469                 // very first notification and if it's not a child of grouped notifications.
470                 row.setSystemExpanded(mAlwaysExpandNonGroupedNotification
471                         || (visibleNotifications == 0 && !isChildNotification
472                         && !row.isLowPriority()));
473             }
474 
475             int userId = entry.getSbn().getUserId();
476             boolean suppressedSummary = mGroupManager.isSummaryOfSuppressedGroup(
477                     entry.getSbn()) && !entry.isRowRemoved();
478             boolean showOnKeyguard = mLockscreenUserManager.shouldShowOnKeyguard(entry);
479             if (!showOnKeyguard) {
480                 // min priority notifications should show if their summary is showing
481                 if (mGroupManager.isChildInGroup(entry)) {
482                     NotificationEntry summary = mGroupManager.getLogicalGroupSummary(entry);
483                     if (summary != null && mLockscreenUserManager.shouldShowOnKeyguard(summary)) {
484                         showOnKeyguard = true;
485                     }
486                 }
487             }
488             if (suppressedSummary
489                     || mLockscreenUserManager.shouldHideNotifications(userId)
490                     || (onKeyguard && !showOnKeyguard)) {
491                 entry.getRow().setVisibility(View.GONE);
492             } else {
493                 boolean wasGone = entry.getRow().getVisibility() == View.GONE;
494                 if (wasGone) {
495                     entry.getRow().setVisibility(View.VISIBLE);
496                 }
497                 if (!isChildNotification && !entry.getRow().isRemoved()) {
498                     if (wasGone) {
499                         // notify the scroller of a child addition
500                         mListContainer.generateAddAnimation(entry.getRow(),
501                                 !showOnKeyguard /* fromMoreCard */);
502                     }
503                     visibleNotifications++;
504                 }
505             }
506             if (row.isSummaryWithChildren()) {
507                 List<ExpandableNotificationRow> notificationChildren =
508                         row.getAttachedChildren();
509                 int size = notificationChildren.size();
510                 for (int i = size - 1; i >= 0; i--) {
511                     stack.push(notificationChildren.get(i));
512                 }
513             }
514             row.showFeedbackIcon(mAssistantFeedbackController.showFeedbackIndicator(entry),
515                     mAssistantFeedbackController.getFeedbackResources(entry));
516             row.setLastAudiblyAlertedMs(entry.getLastAudiblyAlertedMs());
517         }
518 
519         Trace.beginSection("NotificationPresenter#onUpdateRowStates");
520         mPresenter.onUpdateRowStates();
521         Trace.endSection();
522         Trace.endSection();
523     }
524 
525     @Override
onDynamicPrivacyChanged()526     public void onDynamicPrivacyChanged() {
527         mFeatureFlags.assertLegacyPipelineEnabled();
528         if (mPerformingUpdate) {
529             Log.w(TAG, "onDynamicPrivacyChanged made a re-entrant call");
530         }
531         // This listener can be called from updateNotificationViews() via a convoluted listener
532         // chain, so we post here to prevent a re-entrant call. See b/136186188
533         // TODO: Refactor away the need for this
534         if (!mIsHandleDynamicPrivacyChangeScheduled) {
535             mIsHandleDynamicPrivacyChangeScheduled = true;
536             mHandler.post(this::onHandleDynamicPrivacyChanged);
537         }
538     }
539 
onHandleDynamicPrivacyChanged()540     private void onHandleDynamicPrivacyChanged() {
541         mIsHandleDynamicPrivacyChangeScheduled = false;
542         updateNotificationViews();
543     }
544 
beginUpdate()545     private void beginUpdate() {
546         if (mPerformingUpdate) {
547             Log.wtf(TAG, "Re-entrant code during update", new Exception());
548         }
549         mPerformingUpdate = true;
550     }
551 
endUpdate()552     private void endUpdate() {
553         if (!mPerformingUpdate) {
554             Log.wtf(TAG, "Manager state has become desynced", new Exception());
555         }
556         mPerformingUpdate = false;
557     }
558 }
559