diff --git a/animationlib/.gitignore b/animationlib/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..6213826ab5439d99a236ed9afd4dfd29dbb199a9 --- /dev/null +++ b/animationlib/.gitignore @@ -0,0 +1,13 @@ +*.iml +.project +.classpath +.project.properties +gen/ +bin/ +.idea/ +.gradle/ +local.properties +gradle/ +build/ +gradlew* +.DS_Store diff --git a/animationlib/build.gradle b/animationlib/build.gradle index bd5c575e2c542c9d4454edc819c985faa7ac97b9..39ce7a7a446e2e429311952b0f377aa983d97a9d 100644 --- a/animationlib/build.gradle +++ b/animationlib/build.gradle @@ -3,10 +3,6 @@ apply plugin: 'kotlin-android' android { namespace = "com.android.app.animation" - testNamespace = "com.android.app.animation.tests" - defaultConfig { - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } sourceSets { main { @@ -14,30 +10,17 @@ android { res.srcDirs = ['res'] manifest.srcFile 'AndroidManifest.xml' } - androidTest { - java.srcDirs = ["tests/src", "tests/robolectric/src"] - manifest.srcFile 'tests/AndroidManifest.xml' - } } - lintOptions { - abortOnError false - } - tasks.lint.enabled = false - tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" - } - kotlinOptions { - jvmTarget = '1.8' - freeCompilerArgs = ["-Xjvm-default=all"] + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } + + addFrameworkJar('framework-14.jar') } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0" - implementation "androidx.core:core-animation:1.0.0-alpha02" + implementation "androidx.core:core-animation:1.0.0" implementation "androidx.core:core-ktx:1.9.0" - androidTestImplementation libs.robolectric - androidTestImplementation "androidx.test.ext:junit:1.1.3" - androidTestImplementation "androidx.test:rules:1.4.0" } diff --git a/iconloaderlib/build.gradle b/iconloaderlib/build.gradle index 344ac20b5f9bd73a8e72a0d23e0e2394f619d266..5b4c6320dbd55e2cfb188c71d0341e14ab60aedb 100644 --- a/iconloaderlib/build.gradle +++ b/iconloaderlib/build.gradle @@ -1,25 +1,24 @@ -plugins { - id 'com.android.library' -} +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' android { namespace = "com.android.launcher3.icons" sourceSets { main { - java.srcDirs = ['src', 'src_full_lib'] + java.srcDirs = ['src'] manifest.srcFile 'AndroidManifest.xml' res.srcDirs = ['res'] } } - lint { - abortOnError false - } - tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } + + addFrameworkJar('framework-14.jar') } dependencies { - implementation "androidx.core:core" + implementation "androidx.core:core-ktx:1.9.0" } diff --git a/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java b/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java index a42232e3f6d4edd6919865cad070997e28bdc3f1..94f0402b1c35d0bdfb73022ee65cc72201a6949c 100644 --- a/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java +++ b/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java @@ -27,9 +27,12 @@ import android.graphics.Path; import android.graphics.PathMeasure; import android.graphics.Rect; import android.graphics.RectF; +import android.graphics.Typeface; import android.util.Log; import android.view.ViewDebug; +import androidx.core.graphics.ColorUtils; + /** * Used to draw a notification dot on top of an icon. */ @@ -38,10 +41,17 @@ public class DotRenderer { private static final String TAG = "DotRenderer"; // The dot size is defined as a percentage of the app icon size. - private static final float SIZE_PERCENTAGE = 0.228f; + private static final float SIZE_PERCENTAGE = 0.21f; + private static final float SIZE_PERCENTAGE_WITH_COUNT = 0.28f; + + // The max number to draw on dots + private static final int MAX_COUNT = 999; private final float mCircleRadius; private final Paint mCirclePaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG); + private final Paint mCircleShadowPaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG); + + private final Paint mTextPaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG); private final Bitmap mBackgroundWithShadow; private final float mBitmapOffset; @@ -51,14 +61,22 @@ public class DotRenderer { private final float[] mLeftDotPosition; private static final int MIN_DOT_SIZE = 1; + private final Rect mTextRect = new Rect(); + private final boolean mDisplayCount; + public DotRenderer(int iconSizePx, Path iconShapePath, int pathSize) { - int size = Math.round(SIZE_PERCENTAGE * iconSizePx); + this(iconSizePx, iconShapePath, pathSize, false, null); + } + + public DotRenderer(int iconSizePx, Path iconShapePath, int pathSize, Boolean displayCount, Typeface typeface) { + mDisplayCount = displayCount; + int size = Math.round((displayCount ? SIZE_PERCENTAGE_WITH_COUNT : SIZE_PERCENTAGE) * iconSizePx); if (size <= 0) { size = MIN_DOT_SIZE; } ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.TRANSPARENT); builder.ambientShadowAlpha = 88; - mBackgroundWithShadow = builder.setupBlurForSize(size).createPill(size, size); + mBackgroundWithShadow = builder.setupBlurForSize(size).createPill(size, displayCount ? (size - 25) : size); mCircleRadius = builder.radius; mBitmapOffset = -mBackgroundWithShadow.getHeight() * 0.5f; // Same as width. @@ -66,6 +84,10 @@ public class DotRenderer { // Find the points on the path that are closest to the top left and right corners. mLeftDotPosition = getPathPoint(iconShapePath, pathSize, -1); mRightDotPosition = getPathPoint(iconShapePath, pathSize, 1); + + mTextPaint.setTextSize(size * 0.65f); + mTextPaint.setTextAlign(Paint.Align.LEFT); + mTextPaint.setTypeface(typeface); } private static float[] getPathPoint(Path path, float size, float direction) { @@ -101,6 +123,10 @@ public class DotRenderer { * Draw a circle on top of the canvas according to the given params. */ public void draw(Canvas canvas, DrawParams params) { + draw(canvas, params, 0); + } + + public void draw(Canvas canvas, DrawParams params, int numNotifications) { if (params == null) { Log.e(TAG, "Invalid null argument(s) passed in call to draw."); return; @@ -120,16 +146,68 @@ public class DotRenderer { float offsetY = Math.max(0, canvasBounds.top - (dotCenterY + mBitmapOffset)); // We draw the dot relative to its center. - canvas.translate(dotCenterX + offsetX, dotCenterY + offsetY); + float dx = dotCenterX + offsetX; + float dy = dotCenterY + offsetY - 15f; + + if (numNotifications > 9 && numNotifications < 1000) { + canvas.translate(dx - 17f, dy); + } else if (numNotifications > 0) { + canvas.translate(dx - 12f, dy); + } + canvas.scale(params.scale, params.scale); - mCirclePaint.setColor(Color.BLACK); - canvas.drawBitmap(mBackgroundWithShadow, mBitmapOffset, mBitmapOffset, mCirclePaint); mCirclePaint.setColor(params.dotColor); - canvas.drawCircle(0, 0, mCircleRadius, mCirclePaint); + mCircleShadowPaint.setColor(params.shadowDotColor); + + if (numNotifications >= 10 && numNotifications < 1000) { + canvas.drawRoundRect(new RectF(-mCircleRadius + 10, -mCircleRadius, mCircleRadius + 20, mCircleRadius), 50, 50, mCircleShadowPaint); + canvas.drawRoundRect(new RectF(-mCircleRadius + 10, -mCircleRadius, mCircleRadius + 20, mCircleRadius), 50, 50, mCirclePaint); + } else if (numNotifications > 0) { + canvas.drawCircle(5, 10, mCircleRadius, mCircleShadowPaint); + canvas.drawCircle(5, 10, mCircleRadius, mCirclePaint); + } + + if (mDisplayCount && numNotifications > 0) { + // Draw the numNotifications text + mTextPaint.setColor(getCounterTextColor(Color.WHITE)); + mTextPaint.setTypeface(Typeface.DEFAULT_BOLD); + mTextPaint.setTextSize(32f); + String text = numToNotation(numNotifications); + mTextPaint.getTextBounds(text, 0, text.length(), mTextRect); + float y = mTextRect.height() / 2f - mTextRect.bottom; + if (numNotifications < 10) { + canvas.drawText(text, -4f, 22f, mTextPaint); + } else if (numNotifications < 100) { + canvas.drawText(text, -3f, y, mTextPaint); + } else if (numNotifications >= 1000) { + canvas.drawText(text, -14f, 20f, mTextPaint); + } else { + canvas.drawText(text, -12f, y, mTextPaint); + } + } + canvas.restore(); } + private String numToNotation(int num) { + if (num < 1000) { + return String.valueOf(num); + } else { + return num / 1000 + "k"; + } + } + + /** + * Returns the color to use for the counter text based on the dot's background color. + * + * @param dotBackgroundColor The color of the dot background. + * @return The color to use on the counter text. + */ + private int getCounterTextColor(int dotBackgroundColor) { + return ColorUtils.setAlphaComponent(dotBackgroundColor, 0xFF); + } + public static class DrawParams { /** The color (possibly based on the icon) to use for the dot. */ @ViewDebug.ExportedProperty(category = "notification dot", formatToHexString = true) @@ -146,5 +224,7 @@ public class DotRenderer { /** Whether the dot should align to the top left of the icon rather than the top right. */ @ViewDebug.ExportedProperty(category = "notification dot") public boolean leftAlign; + @ViewDebug.ExportedProperty(category = "notification dot", formatToHexString = true) + public int shadowDotColor; } } diff --git a/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java index e8ce3b18a3910876f2583936065d2581f6b6e5d3..2f36d741409c458366197b5a4267c6e20b719694 100644 --- a/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java +++ b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java @@ -69,6 +69,8 @@ public class IconProvider { public static final boolean ATLEAST_T = BuildCompat.isAtLeastT(); private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons"; + private static final String MONTH_BG_ICON_METADATA_KEY_PREFIX = ".month_dynamic_icons"; + private static final String DAY_FG_ICON_METADATA_KEY_PREFIX = ".day_dynamic_icons"; private static final String SYSTEM_STATE_SEPARATOR = " "; @@ -173,7 +175,9 @@ public class IconProvider { .metaData; final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName()); final int id = getDynamicIconId(metadata, resources); - if (id != ID_NULL) { + final int[] monthDayIds = getDynamicBackgroundIconId(metadata, resources); + if (id != ID_NULL && (monthDayIds == null || + monthDayIds[0] == ID_NULL || monthDayIds[1] == ID_NULL )) { if (DEBUG) Log.d(TAG, "Got icon #" + id); Drawable drawable = resources.getDrawableForDensity(id, iconDpi, null /* theme */); if (ATLEAST_T && drawable instanceof AdaptiveIconDrawable && td != null) { @@ -191,6 +195,10 @@ public class IconProvider { } } return drawable; + } else if (monthDayIds != null) { + Drawable background = resources.getDrawableForDensity(monthDayIds[0], iconDpi, null /* theme */); + Drawable foreground = resources.getDrawableForDensity(monthDayIds[1], iconDpi, null /* theme */); + return new AdaptiveIconDrawable(background, foreground); } } catch (PackageManager.NameNotFoundException e) { if (DEBUG) { @@ -225,6 +233,28 @@ public class IconProvider { } } + private int[] getDynamicBackgroundIconId(Bundle metadata, Resources resources) { + if (metadata == null) { + return null; + } + String bgKey = mCalendar.getPackageName() + MONTH_BG_ICON_METADATA_KEY_PREFIX; + String fgKey = mCalendar.getPackageName() + DAY_FG_ICON_METADATA_KEY_PREFIX; + final int bgArrayId = metadata.getInt(bgKey, ID_NULL); + final int fgArrayId = metadata.getInt(fgKey, ID_NULL); + if (bgArrayId == ID_NULL || fgArrayId == ID_NULL) { + return null; + } + try { + return new int[] {resources.obtainTypedArray(bgArrayId).getResourceId(getMonth(), ID_NULL), + resources.obtainTypedArray(fgArrayId).getResourceId(getDay(), ID_NULL)}; + } catch (Resources.NotFoundException e) { + if (DEBUG) { + Log.d(TAG, "package defines '" + bgKey + " and " + fgKey + "' but corresponding array not found"); + } + return null; + } + } + /** * @return Today's day of the month, zero-indexed. */ @@ -232,6 +262,10 @@ public class IconProvider { return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1; } + private static int getMonth() { + return Calendar.getInstance().get(Calendar.MONTH); + } + private static ComponentName parseComponentOrNull(Context context, int resId) { String cn = context.getString(resId); return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn);