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_icon_default_color);
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.time_divider);
132    }
133
134    public void updateChildrenHeaderAppearance() {
135        List<ExpandableNotificationRow> notificationChildren = mRow.getNotificationChildren();
136        if (notificationChildren == null) {
137            return;
138        }
139        // Initialize the comparators
140        for (int compI = 0; compI < mComparators.size(); compI++) {
141            mComparators.get(compI).init();
142        }
143
144        // Compare all notification headers
145        for (int i = 0; i < notificationChildren.size(); i++) {
146            ExpandableNotificationRow row = notificationChildren.get(i);
147            for (int compI = 0; compI < mComparators.size(); compI++) {
148                mComparators.get(compI).compareToHeader(row);
149            }
150        }
151
152        // Apply the comparison to the row
153        for (int i = 0; i < notificationChildren.size(); i++) {
154            ExpandableNotificationRow row = notificationChildren.get(i);
155            for (int compI = 0; compI < mComparators.size(); compI++) {
156                mComparators.get(compI).apply(row);
157            }
158            // We need to sanitize the dividers since they might be off-balance now
159            sanitizeHeaderViews(row);
160        }
161    }
162
163    private void sanitizeHeaderViews(ExpandableNotificationRow row) {
164        if (row.isSummaryWithChildren()) {
165            sanitizeHeader(row.getNotificationHeader());
166            return;
167        }
168        final NotificationContentView layout = row.getPrivateLayout();
169        sanitizeChild(layout.getContractedChild());
170        sanitizeChild(layout.getHeadsUpChild());
171        sanitizeChild(layout.getExpandedChild());
172    }
173
174    private void sanitizeChild(View child) {
175        if (child != null) {
176            NotificationHeaderView header = (NotificationHeaderView) child.findViewById(
177                    com.android.internal.R.id.notification_header);
178            sanitizeHeader(header);
179        }
180    }
181
182    private void sanitizeHeader(NotificationHeaderView rowHeader) {
183        if (rowHeader == null) {
184            return;
185        }
186        final int childCount = rowHeader.getChildCount();
187        View time = rowHeader.findViewById(com.android.internal.R.id.time);
188        boolean hasVisibleText = false;
189        for (int i = 1; i < childCount - 1 ; i++) {
190            View child = rowHeader.getChildAt(i);
191            if (child instanceof TextView
192                    && child.getVisibility() != View.GONE
193                    && !mDividers.contains(Integer.valueOf(child.getId()))
194                    && child != time) {
195                hasVisibleText = true;
196                break;
197            }
198        }
199        // in case no view is visible we make sure the time is visible
200        int timeVisibility = !hasVisibleText
201                || mRow.getStatusBarNotification().getNotification().showsTime()
202                ? View.VISIBLE : View.GONE;
203        time.setVisibility(timeVisibility);
204        View left = null;
205        View right;
206        for (int i = 1; i < childCount - 1 ; i++) {
207            View child = rowHeader.getChildAt(i);
208            if (mDividers.contains(Integer.valueOf(child.getId()))) {
209                boolean visible = false;
210                // Lets find the item to the right
211                for (i++; i < childCount - 1; i++) {
212                    right = rowHeader.getChildAt(i);
213                    if (mDividers.contains(Integer.valueOf(right.getId()))) {
214                        // A divider was found, this needs to be hidden
215                        i--;
216                        break;
217                    } else if (right.getVisibility() != View.GONE && right instanceof TextView) {
218                        visible = left != null;
219                        left = right;
220                        break;
221                    }
222                }
223                child.setVisibility(visible ? View.VISIBLE : View.GONE);
224            } else if (child.getVisibility() != View.GONE && child instanceof TextView) {
225                left = child;
226            }
227        }
228    }
229
230    public void restoreNotificationHeader(ExpandableNotificationRow row) {
231        for (int compI = 0; compI < mComparators.size(); compI++) {
232            mComparators.get(compI).apply(row, true /* reset */);
233        }
234        sanitizeHeaderViews(row);
235    }
236
237    private static class HeaderProcessor {
238        private final int mId;
239        private final DataExtractor mExtractor;
240        private final ResultApplicator mApplicator;
241        private final ExpandableNotificationRow mParentRow;
242        private boolean mApply;
243        private View mParentView;
244        private ViewComparator mComparator;
245        private Object mParentData;
246
247        public static HeaderProcessor forTextView(ExpandableNotificationRow row, int id) {
248            return new HeaderProcessor(row, id, null, sTextViewComparator, sVisibilityApplicator);
249        }
250
251        HeaderProcessor(ExpandableNotificationRow row, int id, DataExtractor extractor,
252                ViewComparator comparator,
253                ResultApplicator applicator) {
254            mId = id;
255            mExtractor = extractor;
256            mApplicator = applicator;
257            mComparator = comparator;
258            mParentRow = row;
259        }
260
261        public void init() {
262            mParentView = mParentRow.getNotificationHeader().findViewById(mId);
263            mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow);
264            mApply = !mComparator.isEmpty(mParentView);
265        }
266        public void compareToHeader(ExpandableNotificationRow row) {
267            if (!mApply) {
268                return;
269            }
270            NotificationHeaderView header = row.getNotificationHeader();
271            if (header == null) {
272                mApply = false;
273                return;
274            }
275            Object childData = mExtractor == null ? null : mExtractor.extractData(row);
276            mApply = mComparator.compare(mParentView, header.findViewById(mId),
277                    mParentData, childData);
278        }
279
280        public void apply(ExpandableNotificationRow row) {
281            apply(row, false /* reset */);
282        }
283
284        public void apply(ExpandableNotificationRow row, boolean reset) {
285            boolean apply = mApply && !reset;
286            if (row.isSummaryWithChildren()) {
287                applyToView(apply, row.getNotificationHeader());
288                return;
289            }
290            applyToView(apply, row.getPrivateLayout().getContractedChild());
291            applyToView(apply, row.getPrivateLayout().getHeadsUpChild());
292            applyToView(apply, row.getPrivateLayout().getExpandedChild());
293        }
294
295        private void applyToView(boolean apply, View parent) {
296            if (parent != null) {
297                View view = parent.findViewById(mId);
298                if (view != null && !mComparator.isEmpty(view)) {
299                    mApplicator.apply(view, apply);
300                }
301            }
302        }
303    }
304
305    private interface ViewComparator {
306        /**
307         * @param parent the parent view
308         * @param child the child view
309         * @param parentData optional data for the parent
310         * @param childData optional data for the child
311         * @return whether to views are the same
312         */
313        boolean compare(View parent, View child, Object parentData, Object childData);
314        boolean isEmpty(View view);
315    }
316
317    private interface DataExtractor {
318        Object extractData(ExpandableNotificationRow row);
319    }
320
321    private static class TextViewComparator implements ViewComparator {
322        @Override
323        public boolean compare(View parent, View child, Object parentData, Object childData) {
324            TextView parentView = (TextView) parent;
325            TextView childView = (TextView) child;
326            return parentView.getText().equals(childView.getText());
327        }
328
329        @Override
330        public boolean isEmpty(View view) {
331            return TextUtils.isEmpty(((TextView) view).getText());
332        }
333    }
334
335    private static abstract class IconComparator implements ViewComparator {
336        @Override
337        public boolean compare(View parent, View child, Object parentData, Object childData) {
338            return false;
339        }
340
341        protected boolean hasSameIcon(Object parentData, Object childData) {
342            Icon parentIcon = ((Notification) parentData).getSmallIcon();
343            Icon childIcon = ((Notification) childData).getSmallIcon();
344            return parentIcon.sameAs(childIcon);
345        }
346
347        /**
348         * @return whether two ImageViews have the same colorFilterSet or none at all
349         */
350        protected boolean hasSameColor(Object parentData, Object childData) {
351            int parentColor = ((Notification) parentData).color;
352            int childColor = ((Notification) childData).color;
353            return parentColor == childColor;
354        }
355
356        @Override
357        public boolean isEmpty(View view) {
358            return false;
359        }
360    }
361
362    private interface ResultApplicator {
363        void apply(View view, boolean apply);
364    }
365
366    private static class VisibilityApplicator implements ResultApplicator {
367
368        @Override
369        public void apply(View view, boolean apply) {
370            view.setVisibility(apply ? View.GONE : View.VISIBLE);
371        }
372    }
373}
374