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
17package com.android.internal.colorextraction;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.app.WallpaperColors;
22import android.app.WallpaperManager;
23import android.content.Context;
24import android.os.Trace;
25import android.os.UserHandle;
26import android.util.Log;
27import android.util.SparseArray;
28
29import com.android.internal.annotations.VisibleForTesting;
30import com.android.internal.colorextraction.types.ExtractionType;
31import com.android.internal.colorextraction.types.Tonal;
32
33import java.lang.ref.WeakReference;
34import java.util.ArrayList;
35import java.util.Iterator;
36
37/**
38 * Class to process wallpaper colors and generate a tonal palette based on them.
39 */
40public class ColorExtractor implements WallpaperManager.OnColorsChangedListener {
41
42    public static final int TYPE_NORMAL = 0;
43    public static final int TYPE_DARK = 1;
44    public static final int TYPE_EXTRA_DARK = 2;
45    private static final int[] sGradientTypes = new int[]{TYPE_NORMAL, TYPE_DARK, TYPE_EXTRA_DARK};
46
47    private static final String TAG = "ColorExtractor";
48    private static final boolean DEBUG = false;
49
50    protected final SparseArray<GradientColors[]> mGradientColors;
51    private final ArrayList<WeakReference<OnColorsChangedListener>> mOnColorsChangedListeners;
52    private final Context mContext;
53    private final ExtractionType mExtractionType;
54    protected WallpaperColors mSystemColors;
55    protected WallpaperColors mLockColors;
56
57    public ColorExtractor(Context context) {
58        this(context, new Tonal(context));
59    }
60
61    @VisibleForTesting
62    public ColorExtractor(Context context, ExtractionType extractionType) {
63        mContext = context;
64        mExtractionType = extractionType;
65
66        mGradientColors = new SparseArray<>();
67        for (int which : new int[] { WallpaperManager.FLAG_LOCK, WallpaperManager.FLAG_SYSTEM}) {
68            GradientColors[] colors = new GradientColors[sGradientTypes.length];
69            mGradientColors.append(which, colors);
70            for (int type : sGradientTypes) {
71                colors[type] = new GradientColors();
72            }
73        }
74
75        mOnColorsChangedListeners = new ArrayList<>();
76        GradientColors[] systemColors = mGradientColors.get(WallpaperManager.FLAG_SYSTEM);
77        GradientColors[] lockColors = mGradientColors.get(WallpaperManager.FLAG_LOCK);
78
79        WallpaperManager wallpaperManager = mContext.getSystemService(WallpaperManager.class);
80        if (wallpaperManager == null) {
81            Log.w(TAG, "Can't listen to color changes!");
82        } else {
83            wallpaperManager.addOnColorsChangedListener(this, null /* handler */);
84
85            // Initialize all gradients with the current colors
86            Trace.beginSection("ColorExtractor#getWallpaperColors");
87            mSystemColors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
88            mLockColors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_LOCK);
89            Trace.endSection();
90        }
91
92        // Initialize all gradients with the current colors
93        extractInto(mSystemColors,
94                systemColors[TYPE_NORMAL],
95                systemColors[TYPE_DARK],
96                systemColors[TYPE_EXTRA_DARK]);
97        extractInto(mLockColors,
98                lockColors[TYPE_NORMAL],
99                lockColors[TYPE_DARK],
100                lockColors[TYPE_EXTRA_DARK]);
101    }
102
103    /**
104     * Retrieve gradient colors for a specific wallpaper.
105     *
106     * @param which FLAG_LOCK or FLAG_SYSTEM
107     * @return colors
108     */
109    @NonNull
110    public GradientColors getColors(int which) {
111        return getColors(which, TYPE_DARK);
112    }
113
114    /**
115     * Get current gradient colors for one of the possible gradient types
116     *
117     * @param which FLAG_LOCK or FLAG_SYSTEM
118     * @param type TYPE_NORMAL, TYPE_DARK or TYPE_EXTRA_DARK
119     * @return colors
120     */
121    @NonNull
122    public GradientColors getColors(int which, int type) {
123        if (type != TYPE_NORMAL && type != TYPE_DARK && type != TYPE_EXTRA_DARK) {
124            throw new IllegalArgumentException(
125                    "type should be TYPE_NORMAL, TYPE_DARK or TYPE_EXTRA_DARK");
126        }
127        if (which != WallpaperManager.FLAG_LOCK && which != WallpaperManager.FLAG_SYSTEM) {
128            throw new IllegalArgumentException("which should be FLAG_SYSTEM or FLAG_NORMAL");
129        }
130        return mGradientColors.get(which)[type];
131    }
132
133    /**
134     * Get the last available WallpaperColors without forcing new extraction.
135     *
136     * @param which FLAG_LOCK or FLAG_SYSTEM
137     * @return Last cached colors
138     */
139    @Nullable
140    public WallpaperColors getWallpaperColors(int which) {
141        if (which == WallpaperManager.FLAG_LOCK) {
142            return mLockColors;
143        } else if (which == WallpaperManager.FLAG_SYSTEM) {
144            return mSystemColors;
145        } else {
146            throw new IllegalArgumentException("Invalid value for which: " + which);
147        }
148    }
149
150    @Override
151    public void onColorsChanged(WallpaperColors colors, int which) {
152        if (DEBUG) {
153            Log.d(TAG, "New wallpaper colors for " + which + ": " + colors);
154        }
155        boolean changed = false;
156        if ((which & WallpaperManager.FLAG_LOCK) != 0) {
157            mLockColors = colors;
158            GradientColors[] lockColors = mGradientColors.get(WallpaperManager.FLAG_LOCK);
159            extractInto(colors, lockColors[TYPE_NORMAL], lockColors[TYPE_DARK],
160                    lockColors[TYPE_EXTRA_DARK]);
161            changed = true;
162        }
163        if ((which & WallpaperManager.FLAG_SYSTEM) != 0) {
164            mSystemColors = colors;
165            GradientColors[] systemColors = mGradientColors.get(WallpaperManager.FLAG_SYSTEM);
166            extractInto(colors, systemColors[TYPE_NORMAL], systemColors[TYPE_DARK],
167                    systemColors[TYPE_EXTRA_DARK]);
168            changed = true;
169        }
170
171        if (changed) {
172            triggerColorsChanged(which);
173        }
174    }
175
176    protected void triggerColorsChanged(int which) {
177        ArrayList<WeakReference<OnColorsChangedListener>> references =
178                new ArrayList<>(mOnColorsChangedListeners);
179        final int size = references.size();
180        for (int i = 0; i < size; i++) {
181            final WeakReference<OnColorsChangedListener> weakReference = references.get(i);
182            final OnColorsChangedListener listener = weakReference.get();
183            if (listener == null) {
184                mOnColorsChangedListeners.remove(weakReference);
185            } else {
186                listener.onColorsChanged(this, which);
187            }
188        }
189    }
190
191    private void extractInto(WallpaperColors inWallpaperColors,
192            GradientColors outGradientColorsNormal, GradientColors outGradientColorsDark,
193            GradientColors outGradientColorsExtraDark) {
194        mExtractionType.extractInto(inWallpaperColors, outGradientColorsNormal,
195                outGradientColorsDark, outGradientColorsExtraDark);
196    }
197
198    public void destroy() {
199        WallpaperManager wallpaperManager = mContext.getSystemService(WallpaperManager.class);
200        if (wallpaperManager != null) {
201            wallpaperManager.removeOnColorsChangedListener(this);
202        }
203    }
204
205    public void addOnColorsChangedListener(@NonNull OnColorsChangedListener listener) {
206        mOnColorsChangedListeners.add(new WeakReference<>(listener));
207    }
208
209    public void removeOnColorsChangedListener(@NonNull OnColorsChangedListener listener) {
210        ArrayList<WeakReference<OnColorsChangedListener>> references =
211                new ArrayList<>(mOnColorsChangedListeners);
212        final int size = references.size();
213        for (int i = 0; i < size; i++) {
214            final WeakReference<OnColorsChangedListener> weakReference = references.get(i);
215            if (weakReference.get() == listener) {
216                mOnColorsChangedListeners.remove(weakReference);
217                break;
218            }
219        }
220    }
221
222    public static class GradientColors {
223        private int mMainColor;
224        private int mSecondaryColor;
225        private boolean mSupportsDarkText;
226
227        public void setMainColor(int mainColor) {
228            mMainColor = mainColor;
229        }
230
231        public void setSecondaryColor(int secondaryColor) {
232            mSecondaryColor = secondaryColor;
233        }
234
235        public void setSupportsDarkText(boolean supportsDarkText) {
236            mSupportsDarkText = supportsDarkText;
237        }
238
239        public void set(GradientColors other) {
240            mMainColor = other.mMainColor;
241            mSecondaryColor = other.mSecondaryColor;
242            mSupportsDarkText = other.mSupportsDarkText;
243        }
244
245        public int getMainColor() {
246            return mMainColor;
247        }
248
249        public int getSecondaryColor() {
250            return mSecondaryColor;
251        }
252
253        public boolean supportsDarkText() {
254            return mSupportsDarkText;
255        }
256
257        @Override
258        public boolean equals(Object o) {
259            if (o == null || o.getClass() != getClass()) {
260                return false;
261            }
262            GradientColors other = (GradientColors) o;
263            return other.mMainColor == mMainColor &&
264                    other.mSecondaryColor == mSecondaryColor &&
265                    other.mSupportsDarkText == mSupportsDarkText;
266        }
267
268        @Override
269        public int hashCode() {
270            int code = mMainColor;
271            code = 31 * code + mSecondaryColor;
272            code = 31 * code + (mSupportsDarkText ? 0 : 1);
273            return code;
274        }
275
276        @Override
277        public String toString() {
278            return "GradientColors(" + Integer.toHexString(mMainColor) + ", "
279                    + Integer.toHexString(mSecondaryColor) + ")";
280        }
281    }
282
283    public interface OnColorsChangedListener {
284        void onColorsChanged(ColorExtractor colorExtractor, int which);
285    }
286}