Adding compat implementation for the new WallpaperManager APIs for color extraction

Change-Id: Ie06c9ac3a77cd33d22ce298a55e234078895c3a0
This commit is contained in:
Sunny Goyal 2017-05-23 14:00:32 -07:00
parent db894b9bba
commit 3e93ec558a
10 changed files with 492 additions and 124 deletions

View File

@ -85,6 +85,11 @@
android:process=":wallpaper_chooser">
</service>
<service
android:name="com.android.launcher3.compat.WallpaperManagerCompatVL$ColorExtractionService"
android:exported="false"
android:process=":wallpaper_chooser" />
<service android:name="com.android.launcher3.notification.NotificationListener"
android:enabled="@bool/notification_badging_enabled"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.compat;
import android.util.SparseIntArray;
/**
* A compatibility layer around platform implementation of WallpaperColors
*/
public class WallpaperColorsCompat {
private final SparseIntArray mColors;
private final boolean mSupportsDarkText;
public WallpaperColorsCompat(SparseIntArray colors, boolean supportsDarkText) {
mColors = colors;
mSupportsDarkText = supportsDarkText;
}
/**
* A map of color code to their occurrences. The bigger the int, the more relevant the color.
*/
public SparseIntArray getColors() {
return mColors;
}
public boolean supportsDarkText() {
return mSupportsDarkText;
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.compat;
import android.content.Context;
import android.support.annotation.Nullable;
import com.android.launcher3.Utilities;
public abstract class WallpaperManagerCompat {
private static final Object sInstanceLock = new Object();
private static WallpaperManagerCompat sInstance;
public static WallpaperManagerCompat getInstance(Context context) {
synchronized (sInstanceLock) {
if (sInstance == null) {
context = context.getApplicationContext();
if (Utilities.isAtLeastO()) {
try {
sInstance = new WallpaperManagerCompatVOMR1(context);
} catch (Exception e) {
// The wallpaper APIs do not yet exist
}
}
if (sInstance == null) {
sInstance = new WallpaperManagerCompatVL(context);
}
}
return sInstance;
}
}
public abstract @Nullable WallpaperColorsCompat getWallpaperColors(int which);
public abstract void addOnColorsChangedListener(OnColorsChangedListenerCompat listener);
/**
* Interface definition for a callback to be invoked when colors change on a wallpaper.
*/
public interface OnColorsChangedListenerCompat {
void onColorsChanged(WallpaperColorsCompat colors, int which);
}
}

View File

@ -0,0 +1,236 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.compat;
import static android.app.WallpaperManager.FLAG_SYSTEM;
import android.app.IntentService;
import android.app.WallpaperInfo;
import android.app.WallpaperManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.os.ResultReceiver;
import android.support.annotation.Nullable;
import android.support.v7.graphics.Palette;
import android.util.Log;
import android.util.Pair;
import android.util.SparseIntArray;
import com.android.launcher3.LauncherFiles;
import com.android.launcher3.Utilities;
import java.io.IOException;
import java.util.ArrayList;
public class WallpaperManagerCompatVL extends WallpaperManagerCompat {
private static final String TAG = "WMCompatVL";
private static final String VERSION_PREFIX = "1,";
private static final String KEY_COLORS = "wallpaper_parsed_colors";
private static final String EXTRA_RECEIVER = "receiver";
private final ArrayList<OnColorsChangedListenerCompat> mListeners = new ArrayList<>();
private final Context mContext;
private WallpaperColorsCompat mColorsCompat;
WallpaperManagerCompatVL(Context context) {
mContext = context;
String colors = prefs(mContext).getString(KEY_COLORS, "");
int wallpaperId = -1;
if (colors.startsWith(VERSION_PREFIX)) {
Pair<Integer, WallpaperColorsCompat> storedValue = parseValue(colors);
wallpaperId = storedValue.first;
mColorsCompat = storedValue.second;
}
if (wallpaperId == -1 || wallpaperId != getWallpaperId(context)) {
reloadColors();
}
context.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
reloadColors();
}
}, new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED));
}
@Nullable
@Override
public WallpaperColorsCompat getWallpaperColors(int which) {
return which == FLAG_SYSTEM ? mColorsCompat : null;
}
@Override
public void addOnColorsChangedListener(OnColorsChangedListenerCompat listener) {
mListeners.add(listener);
}
private void reloadColors() {
ResultReceiver receiver = new ResultReceiver(new Handler()) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
handleResult(resultData.getString(KEY_COLORS));
}
};
mContext.startService(new Intent(mContext, ColorExtractionService.class)
.putExtra(EXTRA_RECEIVER, receiver));
}
private void handleResult(String result) {
prefs(mContext).edit().putString(KEY_COLORS, result).apply();
mColorsCompat = parseValue(result).second;
for (OnColorsChangedListenerCompat listener : mListeners) {
listener.onColorsChanged(mColorsCompat, FLAG_SYSTEM);
}
}
private static SharedPreferences prefs(Context context) {
return context.getSharedPreferences(
LauncherFiles.DEVICE_PREFERENCES_KEY, Context.MODE_PRIVATE);
}
private static final int getWallpaperId(Context context) {
if (!Utilities.ATLEAST_NOUGAT) {
return -1;
}
return context.getSystemService(WallpaperManager.class).getWallpaperId(FLAG_SYSTEM);
}
/**
* Parses the stored value and returns the wallpaper id and wallpaper colors.
*/
private static Pair<Integer, WallpaperColorsCompat> parseValue(String value) {
String[] parts = value.split(",");
Integer wallpaperId = Integer.parseInt(parts[1]);
if (parts.length == 2) {
return Pair.create(wallpaperId, null);
}
SparseIntArray colors = new SparseIntArray((parts.length - 2) / 2);
for (int i = 2; i < parts.length; i += 2) {
colors.put(Integer.parseInt(parts[i]), Integer.parseInt(parts[i + 1]));
}
return Pair.create(wallpaperId, new WallpaperColorsCompat(colors, false));
}
/**
* Intent service to handle color extraction
*/
public static class ColorExtractionService extends IntentService {
private static final int MAX_WALLPAPER_EXTRACTION_AREA = 112 * 112;
public ColorExtractionService() {
super("ColorExtractionService");
}
/**
* Extracts the wallpaper colors and sends the result back through the receiver.
*/
@Override
protected void onHandleIntent(@Nullable Intent intent) {
int wallpaperId = getWallpaperId(this);
Bitmap bitmap = null;
Drawable drawable = null;
WallpaperManager wm = WallpaperManager.getInstance(this);
WallpaperInfo info = wm.getWallpaperInfo();
if (info != null) {
// For live wallpaper, extract colors from thumbnail
drawable = info.loadThumbnail(getPackageManager());
} else {
if (Utilities.ATLEAST_NOUGAT) {
try (ParcelFileDescriptor fd = wm.getWallpaperFile(FLAG_SYSTEM)) {
BitmapRegionDecoder decoder = BitmapRegionDecoder
.newInstance(fd.getFileDescriptor(), false);
int requestedArea = decoder.getWidth() * decoder.getHeight();
BitmapFactory.Options options = new BitmapFactory.Options();
if (requestedArea > MAX_WALLPAPER_EXTRACTION_AREA) {
double areaRatio =
MAX_WALLPAPER_EXTRACTION_AREA / (double) requestedArea;
double nearestPowOf2 =
Math.floor(Math.log(areaRatio) / (2 * Math.log(2)));
options.inSampleSize = (int) Math.pow(2, nearestPowOf2);
}
Rect region = new Rect(0, 0, decoder.getWidth(), decoder.getHeight());
bitmap = decoder.decodeRegion(region, options);
decoder.recycle();
} catch (IOException | NullPointerException e) {
Log.e(TAG, "Fetching partial bitmap failed, trying old method", e);
}
}
if (bitmap == null) {
drawable = wm.getDrawable();
}
}
if (drawable != null) {
// Calculate how big the bitmap needs to be.
// This avoids unnecessary processing and allocation inside Palette.
final int requestedArea = drawable.getIntrinsicWidth() *
drawable.getIntrinsicHeight();
double scale = 1;
if (requestedArea > MAX_WALLPAPER_EXTRACTION_AREA) {
scale = Math.sqrt(MAX_WALLPAPER_EXTRACTION_AREA / (double) requestedArea);
}
bitmap = Bitmap.createBitmap((int) (drawable.getIntrinsicWidth() * scale),
(int) (drawable.getIntrinsicHeight() * scale), Bitmap.Config.ARGB_8888);
final Canvas bmpCanvas = new Canvas(bitmap);
drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
drawable.draw(bmpCanvas);
}
String value = VERSION_PREFIX + wallpaperId;
if (bitmap != null) {
Palette palette = Palette.from(bitmap).generate();
bitmap.recycle();
StringBuilder builder = new StringBuilder(value);
for (Palette.Swatch swatch : palette.getSwatches()) {
builder.append(',')
.append(swatch.getRgb())
.append(',')
.append(swatch.getPopulation());
}
value = builder.toString();
}
ResultReceiver receiver = intent.getParcelableExtra(EXTRA_RECEIVER);
Bundle result = new Bundle();
result.putString(KEY_COLORS, value);
receiver.send(0, result);
}
}
}

View File

@ -0,0 +1,109 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.compat;
import android.annotation.TargetApi;
import android.app.WallpaperManager;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
import android.support.annotation.Nullable;
import android.util.Log;
import android.util.Pair;
import android.util.SparseIntArray;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
@TargetApi(Build.VERSION_CODES.O)
public class WallpaperManagerCompatVOMR1 extends WallpaperManagerCompat {
private static final String TAG = "WMCompatVOMR1";
private final WallpaperManager mWm;
private final Class mOCLClass;
private final Method mAddOCLMethod;
private final Method mWCGetMethod;
private final Method mWCGetColorsMethod;
private final Method mWCSupportsDarkTextMethod;
WallpaperManagerCompatVOMR1(Context context) throws Exception {
mWm = context.getSystemService(WallpaperManager.class);
mOCLClass = Class.forName("android.app.WallpaperManager$OnColorsChangedListener");
mAddOCLMethod = WallpaperManager.class.getDeclaredMethod(
"addOnColorsChangedListener", mOCLClass);
mWCGetMethod = WallpaperManager.class.getDeclaredMethod("getWallpaperColors", int.class);
Class wallpaperColorsClass = mWCGetMethod.getReturnType();
mWCGetColorsMethod = wallpaperColorsClass.getDeclaredMethod("getColors");
mWCSupportsDarkTextMethod = wallpaperColorsClass.getDeclaredMethod("supportsDarkText");
}
@Nullable
@Override
public WallpaperColorsCompat getWallpaperColors(int which) {
try {
return convertColorsObject(mWCGetMethod.invoke(mWm, which));
} catch (Exception e) {
Log.e(TAG, "Error calling wallpaper API", e);
return null;
}
}
@Override
public void addOnColorsChangedListener(final OnColorsChangedListenerCompat listener) {
Object onChangeListener = Proxy.newProxyInstance(
WallpaperManager.class.getClassLoader(),
new Class[]{mOCLClass},
new InvocationHandler() {
@Override
public Object invoke(Object o, Method method, Object[] objects)
throws Throwable {
String methodName = method.getName();
if ("onColorsChanged".equals(methodName)) {
listener.onColorsChanged(
convertColorsObject(objects[0]), (Integer) objects[1]);
} else if ("toString".equals(methodName)) {
return listener.toString();
}
return null;
}
});
try {
mAddOCLMethod.invoke(mWm, onChangeListener);
} catch (Exception e) {
Log.e(TAG, "Error calling wallpaper API", e);
}
}
private WallpaperColorsCompat convertColorsObject(Object colors) throws Exception {
if (colors == null) {
return null;
}
List<Pair<Color, Integer>> list = (List) mWCGetColorsMethod.invoke(colors);
boolean supportsDarkText = (Boolean) mWCSupportsDarkTextMethod.invoke(colors);
SparseIntArray colorMap = new SparseIntArray(list.size());
for (Pair<Color, Integer> color : list) {
colorMap.put(color.first.toArgb(), color.second);
}
return new WallpaperColorsCompat(colorMap, supportsDarkText);
}
}

View File

@ -170,7 +170,7 @@ public class ExtractedColors {
try {
WallpaperManager.class.getDeclaredMethod("getWallpaperColors", int.class);
ColorExtractor extractor = new ColorExtractor(context);
ColorExtractor.GradientColors colors = extractor.getColors(WallpaperManager.FLAG_SYSTEM);
ColorExtractor.GradientColors colors = extractor.getColors();
setColorAtIndex(ALLAPPS_GRADIENT_MAIN_INDEX, colors.getMainColor());
setColorAtIndex(ALLAPPS_GRADIENT_SECONDARY_INDEX, colors.getSecondaryColor());
} catch (NoSuchMethodException e) {

View File

@ -1,16 +1,15 @@
package com.android.launcher3.dynamicui.colorextraction;
import android.app.WallpaperManager;
import static android.app.WallpaperManager.FLAG_SYSTEM;
import android.content.Context;
import android.graphics.Color;
import android.os.Parcelable;
import android.util.Log;
import com.android.launcher3.compat.WallpaperColorsCompat;
import com.android.launcher3.compat.WallpaperManagerCompat;
import com.android.launcher3.dynamicui.colorextraction.types.ExtractionType;
import com.android.launcher3.dynamicui.colorextraction.types.Tonal;
import java.lang.reflect.Method;
/**
* Class to process wallpaper colors and generate a tonal palette based on them.
@ -18,59 +17,32 @@ import java.lang.reflect.Method;
* TODO remove this class if available by platform
*/
public class ColorExtractor {
private static final String TAG = "ColorExtractor";
private static final int FALLBACK_COLOR = Color.WHITE;
private final Context mContext;
private int mMainFallbackColor = FALLBACK_COLOR;
private int mSecondaryFallbackColor = FALLBACK_COLOR;
private final GradientColors mSystemColors;
private final GradientColors mLockColors;
private final Context mContext;
private final ExtractionType mExtractionType;
public ColorExtractor(Context context) {
mContext = context;
mSystemColors = new GradientColors();
mLockColors = new GradientColors();
mExtractionType = new Tonal();
WallpaperManager wallpaperManager = mContext.getSystemService(WallpaperManager.class);
if (wallpaperManager == null) {
Log.w(TAG, "Can't listen to color changes!");
} else {
Parcelable wallpaperColorsObj;
try {
Method method = WallpaperManager.class
.getDeclaredMethod("getWallpaperColors", int.class);
wallpaperColorsObj = (Parcelable) method.invoke(wallpaperManager,
WallpaperManager.FLAG_SYSTEM);
extractInto(new WallpaperColorsCompat(wallpaperColorsObj), mSystemColors);
wallpaperColorsObj = (Parcelable) method.invoke(wallpaperManager,
WallpaperManager.FLAG_LOCK);
extractInto(new WallpaperColorsCompat(wallpaperColorsObj), mLockColors);
} catch (Exception e) {
Log.e(TAG, "reflection failed", e);
}
}
extractFrom(WallpaperManagerCompat.getInstance(context).getWallpaperColors(FLAG_SYSTEM));
}
public GradientColors getColors(int which) {
if (which == WallpaperManager.FLAG_LOCK) {
return mLockColors;
} else if (which == WallpaperManager.FLAG_SYSTEM) {
return mSystemColors;
} else {
throw new IllegalArgumentException("which should be either FLAG_SYSTEM or FLAG_LOCK");
}
public GradientColors getColors() {
return mSystemColors;
}
private void extractInto(WallpaperColorsCompat inWallpaperColors, GradientColors outGradientColors) {
applyFallback(outGradientColors);
private void extractFrom(WallpaperColorsCompat inWallpaperColors) {
applyFallback(mSystemColors);
if (inWallpaperColors == null) {
return;
}
mExtractionType.extractInto(inWallpaperColors, outGradientColors);
mExtractionType.extractInto(inWallpaperColors, mSystemColors);
}
private void applyFallback(GradientColors outGradientColors) {

View File

@ -1,69 +0,0 @@
package com.android.launcher3.dynamicui.colorextraction;
import android.graphics.Color;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Pair;
import java.util.List;
/**
* A wrapper around platform implementation of WallpaperColors until the
* updated SDK is available.
*
* TODO remove this class if available by platform
*/
public class WallpaperColorsCompat implements Parcelable {
private final Parcelable mObject;
public WallpaperColorsCompat(Parcelable object) {
mObject = object;
}
private Object invokeMethod(String methodName) {
try {
return mObject.getClass().getDeclaredMethod(methodName).invoke(mObject);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeParcelable(mObject, i);
}
public static final Parcelable.Creator<WallpaperColorsCompat> CREATOR =
new Parcelable.Creator<WallpaperColorsCompat>() {
public WallpaperColorsCompat createFromParcel(Parcel source) {
Parcelable object = source.readParcelable(null);
return new WallpaperColorsCompat(object);
}
public WallpaperColorsCompat[] newArray(int size) {
return new WallpaperColorsCompat[size];
}
};
public List<Pair<Color, Integer>> getColors() {
try {
return (List<Pair<Color, Integer>>) invokeMethod("getColors");
} catch (Exception e) {
return null;
}
}
public boolean supportsDarkText() {
try {
return (Boolean) invokeMethod("supportsDarkText");
} catch (Exception e) {
return false;
}
}
}

View File

@ -1,7 +1,7 @@
package com.android.launcher3.dynamicui.colorextraction.types;
import com.android.launcher3.compat.WallpaperColorsCompat;
import com.android.launcher3.dynamicui.colorextraction.ColorExtractor;
import com.android.launcher3.dynamicui.colorextraction.WallpaperColorsCompat;
/**

View File

@ -6,11 +6,15 @@ import android.support.annotation.Nullable;
import android.support.v4.graphics.ColorUtils;
import android.util.Log;
import android.util.Pair;
import android.util.SparseIntArray;
import com.android.launcher3.compat.WallpaperColorsCompat;
import com.android.launcher3.dynamicui.colorextraction.ColorExtractor;
import com.android.launcher3.dynamicui.colorextraction.WallpaperColorsCompat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
@ -29,9 +33,10 @@ public class Tonal implements ExtractionType {
private static final float MIN_COLOR_OCCURRENCE = 0.1f;
private static final float MIN_LUMINOSITY = 0.5f;
public void extractInto(WallpaperColorsCompat wallpaperColors,
ColorExtractor.GradientColors gradientColors) {
if (wallpaperColors.getColors().size() == 0) {
public void extractInto(
WallpaperColorsCompat wallpaperColors, ColorExtractor.GradientColors gradientColors) {
SparseIntArray colorsArray = wallpaperColors.getColors();
if (colorsArray.size() == 0) {
return;
}
// Tonal is not really a sort, it takes a color from the extracted
@ -39,24 +44,30 @@ public class Tonal implements ExtractionType {
// palettes. The best fit is tweaked to be closer to the source color
// and replaces the original palette
List<Pair<Integer, Integer>> colors = new ArrayList<>(colorsArray.size());
for (int i = colorsArray.size() - 1; i >= 0; i --) {
colors.add(Pair.create(colorsArray.keyAt(i), colorsArray.valueAt(i)));
}
// First find the most representative color in the image
populationSort(wallpaperColors);
populationSort(colors);
// Calculate total
int total = 0;
for (Pair<Color, Integer> weightedColor : wallpaperColors.getColors()) {
for (Pair<Integer, Integer> weightedColor : colors) {
total += weightedColor.second;
}
// Get bright colors that occur often enough in this image
Pair<Color, Integer> bestColor = null;
Pair<Integer, Integer> bestColor = null;
float[] hsl = new float[3];
for (Pair<Color, Integer> weightedColor : wallpaperColors.getColors()) {
for (Pair<Integer, Integer> weightedColor : colors) {
float colorOccurrence = weightedColor.second / (float) total;
if (colorOccurrence < MIN_COLOR_OCCURRENCE) {
break;
}
int colorValue = weightedColor.first.toArgb();
int colorValue = weightedColor.first;
ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue),
Color.blue(colorValue), hsl);
if (hsl[2] > MIN_LUMINOSITY) {
@ -66,10 +77,10 @@ public class Tonal implements ExtractionType {
// Fallback to first color
if (bestColor == null) {
bestColor = wallpaperColors.getColors().get(0);
bestColor = colors.get(0);
}
int colorValue = bestColor.first.toArgb();
int colorValue = bestColor.first;
ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue), Color.blue(colorValue),
hsl);
hsl[0] /= 360.0f; // normalize
@ -105,10 +116,10 @@ public class Tonal implements ExtractionType {
gradientColors.setSecondaryColor(ColorUtils.HSLToColor(hsl));
}
private static void populationSort(@NonNull WallpaperColorsCompat wallpaperColors) {
wallpaperColors.getColors().sort(new Comparator<Pair<Color, Integer>>() {
private static void populationSort(@NonNull List<Pair<Integer, Integer>> wallpaperColors) {
Collections.sort(wallpaperColors, new Comparator<Pair<Integer, Integer>>() {
@Override
public int compare(Pair<Color, Integer> a, Pair<Color, Integer> b) {
public int compare(Pair<Integer, Integer> a, Pair<Integer, Integer> b) {
return b.second - a.second;
}
});