1/*
2 * Copyright (C) 2015 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
17package com.android.systemui.statusbar;
18
19import android.app.Notification;
20import android.graphics.PorterDuff;
21import android.graphics.drawable.Icon;
22import android.text.TextUtils;
23import android.view.NotificationHeaderView;
24import android.view.View;
25import android.widget.ImageView;
26import android.widget.TextView;
27
28import java.util.ArrayList;
29import java.util.HashSet;
30import java.util.List;
31
32/**
33 * A Util to manage {@link android.view.NotificationHeaderView} objects and their redundancies.
34 */
35public class NotificationHeaderUtil {
36
37    private static final TextViewComparator sTextViewComparator = new TextViewComparator();
38    private static final VisibilityApplicator sVisibilityApplicator = new VisibilityApplicator();
39    private static  final DataExtractor sIconExtractor = new DataExtractor() {
40        @Override
41        public Object extractData(ExpandableNotificationRow row) {
42            return row.getStatusBarNotification().getNotification();
43        }
44    };
45    private static final IconComparator sIconVisibilityComparator = new IconComparator() {
46        public boolean compare(View parent, View child, Object parentData,
47                Object childData) {
48            return hasSameIcon(parentData, childData)
49                    && hasSameColor(parentData, childData);
50        }
51    };
52    private static final IconComparator sGreyComparator = new IconComparator() {
53        public boolean compare(View parent, View child, Object parentData,
54                Object childData) {
55            return !hasSameIcon(parentData, childData)
56                    || hasSameColor(parentData, childData);
57        }
58    };
59    private final static ResultApplicator mGreyApplicator = new ResultApplicator() {
60        @Override
61        public void apply(View view, boolean apply) {
62            NotificationHeaderView header = (NotificationHeaderView) view;
63            ImageView icon = (ImageView) view.findViewById(
64                    com.android.internal.R.id.icon);
65            ImageView expand = (ImageView) view.findViewById(
66                    com.android.internal.R.id.expand_button);
67            applyToChild(icon, apply, header.getOriginalIconColor());
68            applyToChild(expand, apply, header.getOriginalNotificationColor());
69        }
70
71        private void applyToChild(View view, boolean shouldApply, int originalColor) {
72            if (originalColor != NotificationHeaderView.NO_COLOR) {
73                ImageView imageView = (ImageView) view;
74                imageView.getDrawable().mutate();
75                if (shouldApply) {
76                    // lets gray it out
77                    int grey = view.getContext().getColor(
78                            com.android.internal.R.color.notification_default_color_light);
79                    imageView.getDrawable().setColorFilter(grey, PorterDuff.Mode.SRC_ATOP);
80                } else {
81                    // lets reset it
82                    imageView.getDrawable().setColorFilter(originalColor,
83                            PorterDuff.Mode.SRC_ATOP);
84                }
85            }
86        }
87    };
88
89    private final ExpandableNotificationRow mRow;
90    private final ArrayList<HeaderProcessor> mComparators = new ArrayList<>();
91    private final HashSet<Integer> mDividers = new HashSet<>();
92
93    public NotificationHeaderUtil(ExpandableNotificationRow row) {
94        mRow = row;
95        // To hide the icons if they are the same and the color is the same
96        mComparators.add(new HeaderProcessor(mRow,
97                com.android.internal.R.id.icon,
98                sIconExtractor,
99                sIconVisibilityComparator,
100                sVisibilityApplicator));
101        // To grey them out the icons and expand button when the icons are not the same
102        mComparators.add(new HeaderProcessor(mRow,
103                com.android.internal.R.id.notification_header,
104                sIconExtractor,
105                sGreyComparator,
106                mGreyApplicator));
107        mComparators.add(new HeaderProcessor(mRow,
108                com.android.internal.R.id.profile_badge,
109                null /* Extractor */,
110                new ViewComparator() {
111                    @Override
112                    public boolean compare(View parent, View child, Object parentData,
113                            Object childData) {
114                        return parent.getVisibility() != View.GONE;
115                    }
116
117                    @Override
118                    public boolean isEmpty(View view) {
119                        if (view instanceof ImageView) {
120                            return ((ImageView) view).getDrawable() == null;
121                        }
122                        return false;
123                    }
124                },
125                sVisibilityApplicator));
126        mComparators.add(HeaderProcessor.forTextView(mRow,
127                com.android.internal.R.id.app_name_text));
128        mComparators.add(HeaderProcessor.forTextView(mRow,
129                com.android.internal.R.id.header_text));
130        mDividers.add(com.android.internal.R.id.header_text_divider);
131        mDividers.add(com.android.internal.R.id.header_text_secondary_divider);
132        mDividers.add(com.android.internal.R.id.time_divider);
133    }
134
135    public void updateChildrenHeaderAppearance() {
136        List<ExpandableNotificationRow> notificationChildren = mRow.getNotificationChildren();
137        if (notificationChildren == null) {
138            return;
139        }
140        // Initialize the comparators
141        for (int compI = 0; compI < mComparators.size(); compI++) {
142            mComparators.get(compI).init();
143        }
144
145        // Compare all notification headers
146        for (int i = 0; i < notificationChildren.size(); i++) {
147            ExpandableNotificationRow row = notificationChildren.get(i);
148            for (int compI = 0; compI < mComparators.size(); compI++) {
149                mComparators.get(compI).compareToHeader(row);
150            }
151        }
152
153        // Apply the comparison to the row
154        for (int i = 0; i < notificationChildren.size(); i++) {
155            ExpandableNotificationRow row = notificationChildren.get(i);
156            for (int compI = 0; compI < mComparators.size(); compI++) {
157                mComparators.get(compI).apply(row);
158            }
159            // We need to sanitize the dividers since they might be off-balance now
160            sanitizeHeaderViews(row);
161        }
162    }
163
164    private void sanitizeHeaderViews(ExpandableNotificationRow row) {
165        if (row.isSummaryWithChildren()) {
166            sanitizeHeader(row.getNotificationHeader());
167            return;
168        }
169        final NotificationContentView layout = row.getPrivateLayout();
170        sanitizeChild(layout.getContractedChild());
171        sanitizeChild(layout.getHeadsUpChild());
172        sanitizeChild(layout.getExpandedChild());
173    }
174
175    private void sanitizeChild(View child) {
176        if (child != null) {
177            NotificationHeaderView header = (NotificationHeaderView) child.findViewById(
178                    com.android.internal.R.id.notification_header);
179            sanitizeHeader(header);
180        }
181    }
182
183    private void sanitizeHeader(NotificationHeaderView rowHeader) {
184        if (rowHeader == null) {
185            return;
186        }
187        final int childCount = rowHeader.getChildCount();
188        View time = rowHeader.findViewById(com.android.internal.R.id.time);
189        boolean hasVisibleText = false;
190        for (int i = 1; i < childCount - 1 ; i++) {
191            View child = rowHeader.getChildAt(i);
192            if (child instanceof TextView
193                    && child.getVisibility() != View.GONE
194                    && !mDividers.contains(Integer.valueOf(child.getId()))
195                    && child != time) {
196                hasVisibleText = true;
197                break;
198            }
199        }
200        // in case no view is visible we make sure the time is visible
201        int timeVisibility = !hasVisibleText
202                || mRow.getStatusBarNotification().getNotification().showsTime()
203                ? View.VISIBLE : View.GONE;
204        time.setVisibility(timeVisibility);
205        View left = null;
206        View right;
207        for (int i = 1; i < childCount - 1 ; i++) {
208            View child = rowHeader.getChildAt(i);
209            if (mDividers.contains(Integer.valueOf(child.getId()))) {
210                boolean visible = false;
211                // Lets find the item to the right
212                for (i++; i < childCount - 1; i++) {
213                    right = rowHeader.getChildAt(i);
214                    if (mDividers.contains(Integer.valueOf(right.getId()))) {
215                        // A divider was found, this needs to be hidden
216                        i--;
217                        break;
218                    } else if (right.getVisibility() != View.GONE && right instanceof TextView) {
219                        visible = left != null;
220                        left = right;
221                        break;
222                    }
223                }
224                child.setVisibility(visible ? View.VISIBLE : View.GONE);
225            } else if (child.getVisibility() != View.GONE && child instanceof TextView) {
226                left = child;
227            }
228        }
229    }
230
231    public void restoreNotificationHeader(ExpandableNotificationRow row) {
232        for (int compI = 0; compI < mComparators.size(); compI++) {
233            mComparators.get(compI).apply(row, true /* reset */);
234        }
235        sanitizeHeaderViews(row);
236    }
237
238    private static class HeaderProcessor {
239        private final int mId;
240        private final DataExtractor mExtractor;
241        private final ResultApplicator mApplicator;
242        private final ExpandableNotificationRow mParentRow;
243        private boolean mApply;
244        private View mParentView;
245        private ViewComparator mComparator;
246        private Object mParentData;
247
248        public static HeaderProcessor forTextView(ExpandableNotificationRow row, int id) {
249            return new HeaderProcessor(row, id, null, sTextViewComparator, sVisibilityApplicator);
250        }
251
252        HeaderProcessor(ExpandableNotificationRow row, int id, DataExtractor extractor,
253                ViewComparator comparator,
254                ResultApplicator applicator) {
255            mId = id;
256            mExtractor = extractor;
257            mApplicator = applicator;
258            mComparator = comparator;
259            mParentRow = row;
260        }
261
262        public void init() {
263            mParentView = mParentRow.getNotificationHeader().findViewById(mId);
264            mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow);
265            mApply = !mComparator.isEmpty(mParentView);
266        }
267        public void compareToHeader(ExpandableNotificationRow row) {
268            if (!mApply) {
269                return;
270            }
271            NotificationHeaderView header = row.getContractedNotificationHeader();
272            if (header == null) {
273                // No header found. We still consider this to be the same to avoid weird flickering
274                // when for example showing an undo notification
275                return;
276            }
277            Object childData = mExtractor == null ? null : mExtractor.extractData(row);
278            mApply = mComparator.compare(mParentView, header.findViewById(mId),
279                    mParentData, childData);
280        }
281
282        public void apply(ExpandableNotificationRow row) {
283            apply(row, false /* reset */);
284        }
285
286        public void apply(ExpandableNotificationRow row, boolean reset) {
287            boolean apply = mApply && !reset;
288            if (row.isSummaryWithChildren()) {
289                applyToView(apply, row.getNotificationHeader());
290                return;
291            }
292            applyToView(apply, row.getPrivateLayout().getContractedChild());
293            applyToView(apply, row.getPrivateLayout().getHeadsUpChild());
294            applyToView(apply, row.getPrivateLayout().getExpandedChild());
295        }
296
297        private void applyToView(boolean apply, View parent) {
298            if (parent != null) {
299                View view = parent.findViewById(mId);
300                if (view != null && !mComparator.isEmpty(view)) {
301                    mApplicator.apply(view, apply);
302                }
303            }
304        }
305    }
306
307    private interface ViewComparator {
308        /**
309         * @param parent the parent view
310         * @param child the child view
311         * @param parentData optional data for the parent
312         * @param childData optional data for the child
313         * @return whether to views are the same
314         */
315        boolean compare(View parent, View child, Object parentData, Object childData);
316        boolean isEmpty(View view);
317    }
318
319    private interface DataExtractor {
320        Object extractData(ExpandableNotificationRow row);
321    }
322
323    private static class TextViewComparator implements ViewComparator {
324        @Override
325        public boolean compare(View parent, View child, Object parentData, Object childData) {
326            TextView parentView = (TextView) parent;
327            TextView childView = (TextView) child;
328            return parentView.getText().equals(childView.getText());
329        }
330
331        @Override
332        public boolean isEmpty(View view) {
333            return TextUtils.isEmpty(((TextView) view).getText());
334        }
335    }
336
337    private static abstract class IconComparator implements ViewComparator {
338        @Override
339        public boolean compare(View parent, View child, Object parentData, Object childData) {
340            return false;
341        }
342
343        protected boolean hasSameIcon(Object parentData, Object childData) {
344            Icon parentIcon = ((Notification) parentData).getSmallIcon();
345            Icon childIcon = ((Notification) childData).getSmallIcon();
346            return parentIcon.sameAs(childIcon);
347        }
348
349        /**
350         * @return whether two ImageViews have the same colorFilterSet or none at all
351         */
352        protected boolean hasSameColor(Object parentData, Object childData) {
353            int parentColor = ((Notification) parentData).color;
354            int childColor = ((Notification) childData).color;
355            return parentColor == childColor;
356        }
357
358        @Override
359        public boolean isEmpty(View view) {
360            return false;
361        }
362    }
363
364    private interface ResultApplicator {
365        void apply(View view, boolean apply);
366    }
367
368    private static class VisibilityApplicator implements ResultApplicator {
369
370        @Override
371        public void apply(View view, boolean apply) {
372            view.setVisibility(apply ? View.GONE : View.VISIBLE);
373        }
374    }
375}
376