* author: Blankj
* blog : http://blankj.com
* usage : https://www.jianshu.com/p/509b0d2626f4
* time : 16/12/13
* desc : utils about span
*
*/
public final class SpanUtils {
private static final int COLOR_DEFAULT = 0xFEFFFFFF;
public static final int ALIGN_BOTTOM = 0;
public static final int ALIGN_BASELINE = 1;
public static final int ALIGN_CENTER = 2;
public static final int ALIGN_TOP = 3;
@IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER, ALIGN_TOP})
@Retention(RetentionPolicy.SOURCE)
public @interface Align {
}
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
private CharSequence mText;
private int flag;
private int foregroundColor;
private int backgroundColor;
private int lineHeight;
private int alignLine;
private int quoteColor;
private int stripeWidth;
private int quoteGapWidth;
private int first;
private int rest;
private int bulletColor;
private int bulletRadius;
private int bulletGapWidth;
private int fontSize;
private boolean fontSizeIsDp;
private float proportion;
private float xProportion;
private boolean isStrikethrough;
private boolean isUnderline;
private boolean isSuperscript;
private boolean isSubscript;
private boolean isBold;
private boolean isItalic;
private boolean isBoldItalic;
private String fontFamily;
private Typeface typeface;
private Alignment alignment;
private ClickableSpan clickSpan;
private String url;
private float blurRadius;
private Blur style;
private Shader shader;
private float shadowRadius;
private float shadowDx;
private float shadowDy;
private int shadowColor;
private Object[] spans;
private Bitmap imageBitmap;
private Drawable imageDrawable;
private Uri imageUri;
private int imageResourceId;
private int alignImage;
private int spaceSize;
private int spaceColor;
private SpannableStringBuilder mBuilder;
private int mType;
private final int mTypeCharSequence = 0;
private final int mTypeImage = 1;
private final int mTypeSpace = 2;
private Context mContext;
public SpanUtils(Context context) {
mContext = context.getApplicationContext();
mBuilder = new SpannableStringBuilder();
mText = "";
setDefault();
}
private void setDefault() {
flag = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
foregroundColor = COLOR_DEFAULT;
backgroundColor = COLOR_DEFAULT;
lineHeight = -1;
quoteColor = COLOR_DEFAULT;
first = -1;
bulletColor = COLOR_DEFAULT;
fontSize = -1;
proportion = -1;
xProportion = -1;
isStrikethrough = false;
isUnderline = false;
isSuperscript = false;
isSubscript = false;
isBold = false;
isItalic = false;
isBoldItalic = false;
fontFamily = null;
typeface = null;
alignment = null;
clickSpan = null;
url = null;
blurRadius = -1;
shader = null;
shadowRadius = -1;
spans = null;
imageBitmap = null;
imageDrawable = null;
imageUri = null;
imageResourceId = -1;
spaceSize = -1;
}
/**
* Set the span of flag.
*
* @param flag
* The flag.
*
*
{@link Spanned#SPAN_INCLUSIVE_EXCLUSIVE}
*
{@link Spanned#SPAN_INCLUSIVE_INCLUSIVE}
*
{@link Spanned#SPAN_EXCLUSIVE_EXCLUSIVE}
*
{@link Spanned#SPAN_EXCLUSIVE_INCLUSIVE}
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setFlag(final int flag) {
this.flag = flag;
return this;
}
/**
* Set the span of foreground's color.
*
* @param color
* The color of foreground
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setForegroundColor(@ColorInt final int color) {
this.foregroundColor = color;
return this;
}
/**
* Set the span of background's color.
*
* @param color
* The color of background
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setBackgroundColor(@ColorInt final int color) {
this.backgroundColor = color;
return this;
}
/**
* Set the span of line height.
*
* @param lineHeight
* The line height, in pixel.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setLineHeight(@IntRange(from = 0) final int lineHeight) {
return setLineHeight(lineHeight, ALIGN_CENTER);
}
/**
* Set the span of line height.
*
* @param lineHeight
* The line height, in pixel.
* @param align
* The alignment.
*
*
{@link Align#ALIGN_TOP }
*
{@link Align#ALIGN_CENTER}
*
{@link Align#ALIGN_BOTTOM}
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setLineHeight(@IntRange(from = 0) final int lineHeight,
@Align final int align) {
this.lineHeight = lineHeight;
this.alignLine = align;
return this;
}
/**
* Set the span of quote's color.
*
* @param color
* The color of quote
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setQuoteColor(@ColorInt final int color) {
return setQuoteColor(color, 2, 2);
}
/**
* Set the span of quote's color.
*
* @param color
* The color of quote.
* @param stripeWidth
* The width of stripe, in pixel.
* @param gapWidth
* The width of gap, in pixel.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setQuoteColor(@ColorInt final int color,
@IntRange(from = 1) final int stripeWidth,
@IntRange(from = 0) final int gapWidth) {
this.quoteColor = color;
this.stripeWidth = stripeWidth;
this.quoteGapWidth = gapWidth;
return this;
}
/**
* Set the span of leading margin.
*
* @param first
* The indent for the first line of the paragraph.
* @param rest
* The indent for the remaining lines of the paragraph.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setLeadingMargin(@IntRange(from = 0) final int first,
@IntRange(from = 0) final int rest) {
this.first = first;
this.rest = rest;
return this;
}
/**
* Set the span of bullet.
*
* @param gapWidth
* The width of gap, in pixel.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setBullet(@IntRange(from = 0) final int gapWidth) {
return setBullet(0, 3, gapWidth);
}
/**
* Set the span of bullet.
*
* @param color
* The color of bullet.
* @param radius
* The radius of bullet, in pixel.
* @param gapWidth
* The width of gap, in pixel.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setBullet(@ColorInt final int color,
@IntRange(from = 0) final int radius,
@IntRange(from = 0) final int gapWidth) {
this.bulletColor = color;
this.bulletRadius = radius;
this.bulletGapWidth = gapWidth;
return this;
}
/**
* Set the span of font's size.
*
* @param size
* The size of font.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setFontSize(@IntRange(from = 0) final int size) {
return setFontSize(size, false);
}
/**
* Set the span of size of font.
*
* @param size
* The size of font.
* @param isSp
* True to use sp, false to use pixel.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setFontSize(@IntRange(from = 0) final int size, final boolean isSp) {
this.fontSize = size;
this.fontSizeIsDp = isSp;
return this;
}
/**
* Set the span of proportion of font.
*
* @param proportion
* The proportion of font.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setFontProportion(final float proportion) {
this.proportion = proportion;
return this;
}
/**
* Set the span of transverse proportion of font.
*
* @param proportion
* The transverse proportion of font.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setFontXProportion(final float proportion) {
this.xProportion = proportion;
return this;
}
/**
* Set the span of strikethrough.
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setStrikethrough() {
this.isStrikethrough = true;
return this;
}
/**
* Set the span of underline.
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setUnderline() {
this.isUnderline = true;
return this;
}
/**
* Set the span of superscript.
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setSuperscript() {
this.isSuperscript = true;
return this;
}
/**
* Set the span of subscript.
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setSubscript() {
this.isSubscript = true;
return this;
}
/**
* Set the span of bold.
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setBold() {
isBold = true;
return this;
}
/**
* Set the span of bold.
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setNotBold() {
isBold = false;
return this;
}
/**
* Set the span of italic.
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setItalic() {
isItalic = true;
return this;
}
/**
* Set the span of bold italic.
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setBoldItalic() {
isBoldItalic = true;
return this;
}
/**
* Set the span of font family.
*
* @param fontFamily
* The font family.
*
*
monospace
*
serif
*
sans-serif
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setFontFamily(@NonNull final String fontFamily) {
this.fontFamily = fontFamily;
return this;
}
/**
* Set the span of typeface.
*
* @param typeface
* The typeface.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setTypeface(@NonNull final Typeface typeface) {
this.typeface = typeface;
return this;
}
/**
* Set the span of alignment.
*
* @param alignment
* The alignment.
*
*
{@link Alignment#ALIGN_NORMAL }
*
{@link Alignment#ALIGN_OPPOSITE}
*
{@link Alignment#ALIGN_CENTER }
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setAlign(@NonNull final Alignment alignment) {
this.alignment = alignment;
return this;
}
/**
* Set the span of click.
*
Must set {@code view.setMovementMethod(LinkMovementMethod.getInstance())}
*
* @param clickSpan
* The span of click.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setClickSpan(@NonNull final ClickableSpan clickSpan) {
this.clickSpan = clickSpan;
return this;
}
/**
* Set the span of url.
*
Must set {@code view.setMovementMethod(LinkMovementMethod.getInstance())}
*
* @param url
* The url.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setUrl(@NonNull final String url) {
this.url = url;
return this;
}
/**
* Set the span of blur.
*
* @param radius
* The radius of blur.
* @param style
* The style.
*
*
{@link Blur#NORMAL}
*
{@link Blur#SOLID}
*
{@link Blur#OUTER}
*
{@link Blur#INNER}
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setBlur(@FloatRange(from = 0, fromInclusive = false) final float radius,
final Blur style) {
this.blurRadius = radius;
this.style = style;
return this;
}
/**
* Set the span of shader.
*
* @param shader
* The shader.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setShader(@NonNull final Shader shader) {
this.shader = shader;
return this;
}
/**
* Set the span of shadow.
*
* @param radius
* The radius of shadow.
* @param dx
* X-axis offset, in pixel.
* @param dy
* Y-axis offset, in pixel.
* @param shadowColor
* The color of shadow.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setShadow(@FloatRange(from = 0, fromInclusive = false) final float radius,
final float dx,
final float dy,
final int shadowColor) {
this.shadowRadius = radius;
this.shadowDx = dx;
this.shadowDy = dy;
this.shadowColor = shadowColor;
return this;
}
/**
* Set the spans.
*
* @param spans
* The spans.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils setSpans(@NonNull final Object... spans) {
if (spans.length > 0) {
this.spans = spans;
}
return this;
}
/**
* Append the text text.
*
* @param text
* The text.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils append(@NonNull final CharSequence text) {
apply(mTypeCharSequence);
mText = text;
return this;
}
/**
* Append one line.
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils appendLine() {
apply(mTypeCharSequence);
mText = LINE_SEPARATOR;
return this;
}
/**
* Append text and one line.
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils appendLine(@NonNull final CharSequence text) {
apply(mTypeCharSequence);
mText = text + LINE_SEPARATOR;
return this;
}
/**
* Append one image.
*
* @param bitmap
* The bitmap of image.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils appendImage(@NonNull final Bitmap bitmap) {
return appendImage(bitmap, ALIGN_BOTTOM);
}
/**
* Append one image.
*
* @param bitmap
* The bitmap.
* @param align
* The alignment.
*
*
{@link Align#ALIGN_TOP }
*
{@link Align#ALIGN_CENTER }
*
{@link Align#ALIGN_BASELINE}
*
{@link Align#ALIGN_BOTTOM }
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils appendImage(@NonNull final Bitmap bitmap, @Align final int align) {
apply(mTypeImage);
this.imageBitmap = bitmap;
this.alignImage = align;
return this;
}
/**
* Append one image.
*
* @param drawable
* The drawable of image.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils appendImage(@NonNull final Drawable drawable) {
return appendImage(drawable, ALIGN_BOTTOM);
}
/**
* Append one image.
*
* @param drawable
* The drawable of image.
* @param align
* The alignment.
*
*
{@link Align#ALIGN_TOP }
*
{@link Align#ALIGN_CENTER }
*
{@link Align#ALIGN_BASELINE}
*
{@link Align#ALIGN_BOTTOM }
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils appendImage(@NonNull final Drawable drawable, @Align final int align) {
apply(mTypeImage);
this.imageDrawable = drawable;
this.alignImage = align;
return this;
}
/**
* Append one image.
*
* @param uri
* The uri of image.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils appendImage(@NonNull final Uri uri) {
return appendImage(uri, ALIGN_BOTTOM);
}
/**
* Append one image.
*
* @param uri
* The uri of image.
* @param align
* The alignment.
*
*
{@link Align#ALIGN_TOP }
*
{@link Align#ALIGN_CENTER }
*
{@link Align#ALIGN_BASELINE}
*
{@link Align#ALIGN_BOTTOM }
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils appendImage(@NonNull final Uri uri, @Align final int align) {
apply(mTypeImage);
this.imageUri = uri;
this.alignImage = align;
return this;
}
/**
* Append one image.
*
* @param resourceId
* The resource id of image.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils appendImage(@DrawableRes final int resourceId) {
return appendImage(resourceId, ALIGN_BOTTOM);
}
/**
* Append one image.
*
* @param resourceId
* The resource id of image.
* @param align
* The alignment.
*
*
{@link Align#ALIGN_TOP }
*
{@link Align#ALIGN_CENTER }
*
{@link Align#ALIGN_BASELINE}
*
{@link Align#ALIGN_BOTTOM }
*
* @return the single {@link SpanUtils} instance
*/
public SpanUtils appendImage(@DrawableRes final int resourceId, @Align final int align) {
append(Character.toString((char) 0));// it's important for span start with image
apply(mTypeImage);
this.imageResourceId = resourceId;
this.alignImage = align;
return this;
}
/**
* Append space.
*
* @param size
* The size of space.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils appendSpace(@IntRange(from = 0) final int size) {
return appendSpace(size, Color.TRANSPARENT);
}
/**
* Append space.
*
* @param size
* The size of space.
* @param color
* The color of space.
* @return the single {@link SpanUtils} instance
*/
public SpanUtils appendSpace(@IntRange(from = 0) final int size, @ColorInt final int color) {
apply(mTypeSpace);
spaceSize = size;
spaceColor = color;
return this;
}
private void apply(final int type) {
applyLast();
mType = type;
}
/**
* Create the span string.
*
* @return the span string
*/
public SpannableStringBuilder create() {
applyLast();
return mBuilder;
}
private void applyLast() {
if (mType == mTypeCharSequence) {
updateCharCharSequence();
} else if (mType == mTypeImage) {
updateImage();
} else if (mType == mTypeSpace) {
updateSpace();
}
setDefault();
}
private void updateCharCharSequence() {
if (mText.length() == 0) return;
int start = mBuilder.length();
mBuilder.append(mText);
int end = mBuilder.length();
if (foregroundColor != COLOR_DEFAULT) {
mBuilder.setSpan(new ForegroundColorSpan(foregroundColor), start, end, flag);
}
if (backgroundColor != COLOR_DEFAULT) {
mBuilder.setSpan(new BackgroundColorSpan(backgroundColor), start, end, flag);
}
if (first != -1) {
mBuilder.setSpan(new LeadingMarginSpan.Standard(first, rest), start, end, flag);
}
if (quoteColor != COLOR_DEFAULT) {
mBuilder.setSpan(
new CustomQuoteSpan(quoteColor, stripeWidth, quoteGapWidth),
start,
end,
flag
);
}
if (bulletColor != COLOR_DEFAULT) {
mBuilder.setSpan(
new CustomBulletSpan(bulletColor, bulletRadius, bulletGapWidth),
start,
end,
flag
);
}
// if (imGapWidth != -1) {
// if (imBitmap != null) {
// mBuilder.setSpan(
// new CustomIconMarginSpan(imBitmap, imGapWidth, imAlign),
// start,
// end,
// flag
// );
// } else if (imDrawable != null) {
// mBuilder.setSpan(
// new CustomIconMarginSpan(imDrawable, imGapWidth, imAlign),
// start,
// end,
// flag
// );
// } else if (imUri != null) {
// mBuilder.setSpan(
// new CustomIconMarginSpan(imUri, imGapWidth, imAlign),
// start,
// end,
// flag
// );
// } else if (imResourceId != -1) {
// mBuilder.setSpan(
// new CustomIconMarginSpan(imResourceId, imGapWidth, imAlign),
// start,
// end,
// flag
// );
// }
// }
if (fontSize != -1) {
mBuilder.setSpan(new AbsoluteSizeSpan(fontSize, fontSizeIsDp), start, end, flag);
}
if (proportion != -1) {
mBuilder.setSpan(new RelativeSizeSpan(proportion), start, end, flag);
}
if (xProportion != -1) {
mBuilder.setSpan(new ScaleXSpan(xProportion), start, end, flag);
}
if (lineHeight != -1) {
mBuilder.setSpan(new CustomLineHeightSpan(lineHeight, alignLine), start, end, flag);
}
if (isStrikethrough) {
mBuilder.setSpan(new StrikethroughSpan(), start, end, flag);
}
if (isUnderline) {
mBuilder.setSpan(new UnderlineSpan(), start, end, flag);
}
if (isSuperscript) {
mBuilder.setSpan(new SuperscriptSpan(), start, end, flag);
}
if (isSubscript) {
mBuilder.setSpan(new SubscriptSpan(), start, end, flag);
}
if (isBold) {
mBuilder.setSpan(new StyleSpan(Typeface.BOLD), start, end, flag);
}
if (isItalic) {
mBuilder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flag);
}
if (isBoldItalic) {
mBuilder.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end, flag);
}
if (fontFamily != null) {
mBuilder.setSpan(new TypefaceSpan(fontFamily), start, end, flag);
}
if (typeface != null) {
mBuilder.setSpan(new CustomTypefaceSpan(typeface), start, end, flag);
}
if (alignment != null) {
mBuilder.setSpan(new AlignmentSpan.Standard(alignment), start, end, flag);
}
if (clickSpan != null) {
mBuilder.setSpan(clickSpan, start, end, flag);
}
if (url != null) {
mBuilder.setSpan(new URLSpan(url), start, end, flag);
}
if (blurRadius != -1) {
mBuilder.setSpan(
new MaskFilterSpan(new BlurMaskFilter(blurRadius, style)),
start,
end,
flag
);
}
if (shader != null) {
mBuilder.setSpan(new ShaderSpan(shader), start, end, flag);
}
if (shadowRadius != -1) {
mBuilder.setSpan(
new ShadowSpan(shadowRadius, shadowDx, shadowDy, shadowColor),
start,
end,
flag
);
}
if (spans != null) {
for (Object span : spans) {
mBuilder.setSpan(span, start, end, flag);
}
}
}
private void updateImage() {
int start = mBuilder.length();
mBuilder.append("");
int end = start + 5;
if (imageBitmap != null) {
mBuilder.setSpan(new CustomImageSpan(imageBitmap, alignImage), start, end, flag);
} else if (imageDrawable != null) {
mBuilder.setSpan(new CustomImageSpan(imageDrawable, alignImage), start, end, flag);
} else if (imageUri != null) {
mBuilder.setSpan(new CustomImageSpan(imageUri, alignImage), start, end, flag);
} else if (imageResourceId != -1) {
mBuilder.setSpan(new CustomImageSpan(imageResourceId, alignImage), start, end, flag);
}
}
private void updateSpace() {
int start = mBuilder.length();
mBuilder.append("< >");
int end = start + 3;
mBuilder.setSpan(new SpaceSpan(spaceSize, spaceColor), start, end, flag);
}
class CustomLineHeightSpan extends CharacterStyle
implements LineHeightSpan {
private final int height;
static final int ALIGN_CENTER = 2;
static final int ALIGN_TOP = 3;
final int mVerticalAlignment;
CustomLineHeightSpan(int height, int verticalAlignment) {
this.height = height;
mVerticalAlignment = verticalAlignment;
}
@Override
public void chooseHeight(final CharSequence text, final int start, final int end,
final int spanstartv, final int v, final Paint.FontMetricsInt fm) {
int need = height - (v + fm.descent - fm.ascent - spanstartv);
// if (need > 0) {
if (mVerticalAlignment == ALIGN_TOP) {
fm.descent += need;
} else if (mVerticalAlignment == ALIGN_CENTER) {
fm.descent += need / 2;
fm.ascent -= need / 2;
} else {
fm.ascent -= need;
}
// }
need = height - (v + fm.bottom - fm.top - spanstartv);
// if (need > 0) {
if (mVerticalAlignment == ALIGN_TOP) {
fm.top += need;
} else if (mVerticalAlignment == ALIGN_CENTER) {
fm.bottom += need / 2;
fm.top -= need / 2;
} else {
fm.top -= need;
}
// }
}
@Override
public void updateDrawState(final TextPaint tp) {
}
}
class SpaceSpan extends ReplacementSpan {
private final int width;
private final int color;
private SpaceSpan(final int width) {
this(width, Color.TRANSPARENT);
}
private SpaceSpan(final int width, final int color) {
super();
this.width = width;
this.color = color;
}
@Override
public int getSize(@NonNull final Paint paint, final CharSequence text,
@IntRange(from = 0) final int start,
@IntRange(from = 0) final int end,
@Nullable final Paint.FontMetricsInt fm) {
return width;
}
@Override
public void draw(@NonNull final Canvas canvas, final CharSequence text,
@IntRange(from = 0) final int start,
@IntRange(from = 0) final int end,
final float x, final int top, final int y, final int bottom,
@NonNull final Paint paint) {
Paint.Style style = paint.getStyle();
int color = paint.getColor();
paint.setStyle(Paint.Style.FILL);
paint.setColor(this.color);
canvas.drawRect(x, top, x + width, bottom, paint);
paint.setStyle(style);
paint.setColor(color);
}
}
class CustomQuoteSpan implements LeadingMarginSpan {
private final int color;
private final int stripeWidth;
private final int gapWidth;
private CustomQuoteSpan(final int color, final int stripeWidth, final int gapWidth) {
super();
this.color = color;
this.stripeWidth = stripeWidth;
this.gapWidth = gapWidth;
}
public int getLeadingMargin(final boolean first) {
return stripeWidth + gapWidth;
}
public void drawLeadingMargin(final Canvas c, final Paint p, final int x, final int dir,
final int top, final int baseline, final int bottom,
final CharSequence text, final int start, final int end,
final boolean first, final Layout layout) {
Paint.Style style = p.getStyle();
int color = p.getColor();
p.setStyle(Paint.Style.FILL);
p.setColor(this.color);
c.drawRect(x, top, x + dir * stripeWidth, bottom, p);
p.setStyle(style);
p.setColor(color);
}
}
class CustomBulletSpan implements LeadingMarginSpan {
private final int color;
private final int radius;
private final int gapWidth;
private Path sBulletPath = null;
private CustomBulletSpan(final int color, final int radius, final int gapWidth) {
this.color = color;
this.radius = radius;
this.gapWidth = gapWidth;
}
public int getLeadingMargin(final boolean first) {
return 2 * radius + gapWidth;
}
public void drawLeadingMargin(final Canvas c, final Paint p, final int x, final int dir,
final int top, final int baseline, final int bottom,
final CharSequence text, final int start, final int end,
final boolean first, final Layout l) {
if (((Spanned) text).getSpanStart(this) == start) {
Paint.Style style = p.getStyle();
int oldColor = 0;
oldColor = p.getColor();
p.setColor(color);
p.setStyle(Paint.Style.FILL);
if (c.isHardwareAccelerated()) {
if (sBulletPath == null) {
sBulletPath = new Path();
// Bullet is slightly better to avoid aliasing artifacts on mdpi devices.
sBulletPath.addCircle(0.0f, 0.0f, radius, Path.Direction.CW);
}
c.save();
c.translate(x + dir * radius, (top + bottom) / 2.0f);
c.drawPath(sBulletPath, p);
c.restore();
} else {
c.drawCircle(x + dir * radius, (top + bottom) / 2.0f, radius, p);
}
p.setColor(oldColor);
p.setStyle(style);
}
}
}
@SuppressLint("ParcelCreator")
class CustomTypefaceSpan extends TypefaceSpan {
private final Typeface newType;
private CustomTypefaceSpan(final Typeface type) {
super("");
newType = type;
}
@Override
public void updateDrawState(final TextPaint textPaint) {
apply(textPaint, newType);
}
@Override
public void updateMeasureState(final TextPaint paint) {
apply(paint, newType);
}
private void apply(final Paint paint, final Typeface tf) {
int oldStyle;
Typeface old = paint.getTypeface();
if (old == null) {
oldStyle = 0;
} else {
oldStyle = old.getStyle();
}
int fake = oldStyle & ~tf.getStyle();
if ((fake & Typeface.BOLD) != 0) {
paint.setFakeBoldText(true);
}
if ((fake & Typeface.ITALIC) != 0) {
paint.setTextSkewX(-0.25f);
}
paint.getShader();
paint.setTypeface(tf);
}
}
class CustomImageSpan extends CustomDynamicDrawableSpan {
private Drawable mDrawable;
private Uri mContentUri;
private int mResourceId;
private CustomImageSpan(final Bitmap b, final int verticalAlignment) {
super(verticalAlignment);
mDrawable = new BitmapDrawable(mContext.getResources(), b);
mDrawable.setBounds(
0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()
);
}
private CustomImageSpan(final Drawable d, final int verticalAlignment) {
super(verticalAlignment);
mDrawable = d;
mDrawable.setBounds(
0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()
);
}
private CustomImageSpan(final Uri uri, final int verticalAlignment) {
super(verticalAlignment);
mContentUri = uri;
}
private CustomImageSpan(@DrawableRes final int resourceId, final int verticalAlignment) {
super(verticalAlignment);
mResourceId = resourceId;
}
@Override
public Drawable getDrawable() {
Drawable drawable = null;
if (mDrawable != null) {
drawable = mDrawable;
} else if (mContentUri != null) {
Bitmap bitmap;
try {
InputStream is =
mContext.getContentResolver().openInputStream(mContentUri);
bitmap = BitmapFactory.decodeStream(is);
drawable = new BitmapDrawable(mContext.getResources(), bitmap);
drawable.setBounds(
0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()
);
if (is != null) {
is.close();
}
} catch (Exception e) {
Log.e("sms", "Failed to loaded content " + mContentUri, e);
}
} else {
try {
drawable = ContextCompat.getDrawable(mContext, mResourceId);
drawable.setBounds(
0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()
);
} catch (Exception e) {
Log.e("sms", "Unable to find resource: " + mResourceId);
}
}
return drawable;
}
}
abstract class CustomDynamicDrawableSpan extends ReplacementSpan {
static final int ALIGN_BOTTOM = 0;
static final int ALIGN_BASELINE = 1;
static final int ALIGN_CENTER = 2;
static final int ALIGN_TOP = 3;
final int mVerticalAlignment;
private CustomDynamicDrawableSpan() {
mVerticalAlignment = ALIGN_BOTTOM;
}
private CustomDynamicDrawableSpan(final int verticalAlignment) {
mVerticalAlignment = verticalAlignment;
}
public abstract Drawable getDrawable();
@Override
public int getSize(@NonNull final Paint paint, final CharSequence text,
final int start, final int end, final Paint.FontMetricsInt fm) {
Drawable d = getCachedDrawable();
Rect rect = d.getBounds();
if (fm != null) {
// LogUtils.d("fm.top: " + fm.top,
// "fm.ascent: " + fm.ascent,
// "fm.descent: " + fm.descent,
// "fm.bottom: " + fm.bottom,
// "lineHeight: " + (fm.bottom - fm.top));
int lineHeight = fm.bottom - fm.top;
if (lineHeight < rect.height()) {
if (mVerticalAlignment == ALIGN_TOP) {
fm.top = fm.top;
fm.bottom = rect.height() + fm.top;
} else if (mVerticalAlignment == ALIGN_CENTER) {
fm.top = -rect.height() / 2 - lineHeight / 4;
fm.bottom = rect.height() / 2 - lineHeight / 4;
} else {
fm.top = -rect.height() + fm.bottom;
fm.bottom = fm.bottom;
}
fm.ascent = fm.top;
fm.descent = fm.bottom;
}
}
return rect.right;
}
@Override
public void draw(@NonNull final Canvas canvas, final CharSequence text,
final int start, final int end, final float x,
final int top, final int y, final int bottom, @NonNull final Paint paint) {
Drawable d = getCachedDrawable();
Rect rect = d.getBounds();
canvas.save();
float transY;
int lineHeight = bottom - top;
// LogUtils.d("rectHeight: " + rect.height(),
// "lineHeight: " + (bottom - top));
if (rect.height() < lineHeight) {
if (mVerticalAlignment == ALIGN_TOP) {
transY = top;
} else if (mVerticalAlignment == ALIGN_CENTER) {
transY = (bottom + top - rect.height()) / 2;
} else if (mVerticalAlignment == ALIGN_BASELINE) {
transY = y - rect.height();
} else {
transY = bottom - rect.height();
}
canvas.translate(x, transY);
} else {
canvas.translate(x, top);
}
d.draw(canvas);
canvas.restore();
}
private Drawable getCachedDrawable() {
WeakReference wr = mDrawableRef;
Drawable d = null;
if (wr != null) {
d = wr.get();
}
if (d == null) {
d = getDrawable();
mDrawableRef = new WeakReference<>(d);
}
return d;
}
private WeakReference mDrawableRef;
}
class ShaderSpan extends CharacterStyle implements UpdateAppearance {
private Shader mShader;
private ShaderSpan(final Shader shader) {
this.mShader = shader;
}
@Override
public void updateDrawState(final TextPaint tp) {
tp.setShader(mShader);
}
}
class ShadowSpan extends CharacterStyle implements UpdateAppearance {
private float radius;
private float dx, dy;
private int shadowColor;
private ShadowSpan(final float radius,
final float dx,
final float dy,
final int shadowColor) {
this.radius = radius;
this.dx = dx;
this.dy = dy;
this.shadowColor = shadowColor;
}
@Override
public void updateDrawState(final TextPaint tp) {
tp.setShadowLayer(radius, dx, dy, shadowColor);
}
}
}
================================================
FILE: app/src/main/res/drawable/bg_btn.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_java_demo.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: app/src/main/res/layout/check_md5_demo_activity.xml
================================================
================================================
FILE: app/src/main/res/layout/view_update_dialog_custom.xml
================================================
================================================
FILE: app/src/main/res/values/colors.xml
================================================
#3F51B5#303F9F#FF4081#0076FF#333333
================================================
FILE: app/src/main/res/values/dimens.xml
================================================
16dp16dp
================================================
FILE: app/src/main/res/values/strings.xml
================================================
Update
================================================
FILE: app/src/main/res/values/styles.xml
================================================
================================================
FILE: app/src/main/res/values-w820dp/dimens.xml
================================================
64dp
================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.31'
repositories {
jcenter()
google()
maven { url 'https://jitpack.io' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.novoda:bintray-release:0.9.1'
}
}
allprojects {
repositories {
maven { url 'https://maven.aliyun.com/repository/public/' }
maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' }
maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' }
maven { url 'http://maven.aliyun.com/nexus/content/repositories/gradle-plugin' }
jcenter()
google()
maven { url 'https://jitpack.io' }
}
//中文注释
tasks.withType(Javadoc) {
options{ encoding "UTF-8"
charSet 'UTF-8'
links "http://docs.oracle.com/javase/7/docs/api"
}
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Mon Jun 03 12:27:34 CST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
================================================
FILE: gradlew
================================================
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: readme/README_1.5.2.md
================================================
# updateapputils
### 一行代码,快速实现app在线下载更新 A simple library for Android update app
### 适配Android6.0、7.0、8.0

## 集成
compile引入
```
dependencies {
implementation 'com.teprinciple:updateapputils:1.5.2'
}
```
## 使用
更新检测一般放在MainActivity或者启动页上,
在请求服务器版本检测接口获取到versionCode、versionName、最新apkPath后调用。
#### 快速使用
```
UpdateAppUtils.from(this)
.serverVersionCode(2) //服务器versionCode
.serverVersionName("2.0") //服务器versionName
.apkPath(apkPath) //最新apk下载地址
.update();
```
#### Kotlin代码调用完全一样
```
private fun update() {
val apkPath:String = "http://issuecdn.baidupcs.com/issue/netdisk/apk/BaiduNetdisk_7.15.1.apk"
UpdateAppUtils.from(this)
.serverVersionCode(2)
.serverVersionName("2.0")
.apkPath(apkPath)
.update()
}
```
#### 更多配置使用
```
UpdateAppUtils.from(this)
.checkBy(UpdateAppUtils.CHECK_BY_VERSION_NAME) //更新检测方式,默认为VersionCode
.serverVersionCode(2)
.serverVersionName("2.0")
.apkPath(apkPath)
.showNotification(false) //是否显示下载进度到通知栏,默认为true
.updateInfo(info) //更新日志信息 String
.downloadBy(UpdateAppUtils.DOWNLOAD_BY_BROWSER) //下载方式:app下载、手机浏览器下载。默认app下载
.isForce(true) //是否强制更新,默认false 强制更新情况下用户不同意更新则不能使用app
.update();
```
#### 说明
```
1、UpdateAppUtils提供两种更新判断方式
CHECK_BY_VERSION_CODE:通过versionCode判断,服务器上versionCode > 本地versionCode则执行更新
CHECK_BY_VERSION_NAME:通过versionName判断,服务器上versionName 与 本地versionName不同则更新
2、UpdateAppUtils提供两种下载apk方式
DOWNLOAD_BY_APP:通过App下载
DOWNLOAD_BY_BROWSER:通过手机浏览器下载
```
#### 关于适配Android6.0、7.0、8.0
库内部已经完全适配至8.0,你可以不用再对该库进行适配
#### 文章地址:[《UpdateAppUtils一行代码实现app在线更新》](http://www.jianshu.com/p/9c91bb984c85)
#### 更新日志
1.5.2
修复部分bug
1.5.1
库内部适配至Android8.0
1.4
使用[filedownloader](https://github.com/lingochamp/FileDownloader)替换DownloadManager,避免部分手机DownLoadManager无效,同时解决了重复下载的问题,且提高了下载速度
增加接口UpdateAppUtils.needFitAndroidN(false),避免不需要适配7.0,也要设置FileProvider
1.3.1
修复部分bug,在demo中加入kotlin调用代码
1.3
增加接口方法 showNotification(false)//是否显示下载进度到通知栏; updateInfo(info)//更新日志信息;下载前WiFi判断。
1.2
适配Android7.0,并在demo中加入适配6.0和7.0的代码
1.1
适配更多SdkVersion
================================================
FILE: readme/version.md
================================================
### 更新日志
#### 2.3.0
* 修复部分手机context空指针异常
#### 2.2.1
* 优化代码
* 修复部分bug
#### 2.2.0
* 适配Android 10
* 修复部分bug
#### 2.1.0
* 增加'暂不更新'按钮点击监听 setCancelBtnClickListener()
* 增加'立即更新'按钮点击监听 setUpdateBtnClickListener()
* 修复部分bug
#### 2.0.4
* 修复阿里云,码云平台上的apk FileDownloader下载失败
* 增加UpdateConfig alwaysShowDownLoadDialog字段,让非强更也能显示下载进度弹窗
#### 2.0.3
* 更新弹窗内容支持SpannableString
#### 2.0.2
* 9.0Http适配
#### 2.0.1
* 自定义FileProvide,防止provider冲突
#### 2.0.0
* Kotlin重构
* 支持AndroidX
* 安装包签名文件md5校验
* 通知栏自定义图标
* 支持自定义UI
* 适配中英文
* 增加下载回调等api
* 修复部分bug
#### 1.5.2
* 修复部分bug
#### 1.5.1
* 库内部适配至Android8.0
#### 1.4
* 使用[filedownloader](https://github.com/lingochamp/FileDownloader)替换DownloadManager,避免部分手机DownLoadManager无效,同时解决了重复下载的问题,且提高了下载速度
* 增加接口UpdateAppUtils.needFitAndroidN(false),避免不需要适配7.0,也要设置FileProvider
#### 1.3.1
* 修复部分bug,在demo中加入kotlin调用代码
#### 1.3
* 增加接口方法 showNotification(false) //是否显示下载进度到通知栏;
* updateInfo(info) //更新日志信息;
* 下载前WiFi判断。
#### 1.2
* 适配Android7.0,并在demo中加入适配6.0和7.0的代码
#### 1.1
* 适配更多SdkVersion
================================================
FILE: readme/自定义UI.md
================================================
## 完全自定义UI
### 1、创建你的layout(必须)
你可以创建任意你想要的UI布局([参考 view_update_dialog_custom.xml](https://github.com/teprinciple/UpdateAppUtils/blob/master/app/src/main/res/layout/view_update_dialog_custom.xml))
,但是控件id需要保持如下:
| id | 说明 | 控件类型 | 是否必须 |
|:--------------------- |:-------------------|:----------------- |:------ |
| btn_update_sure | 立即更新按钮id| 任意View |true |
| btn_update_cancel | 暂不更新按钮id| 任意View |true |
| tv_update_title | 更新弹窗标题| TextView |false |
| tv_update_content | 更新内容| TextView |false |
btn_update_sure和btn_update_cancel是必须提供的,否则更新无法继续;
tv_update_title,tv_update_content提供,UpdateAppUtils内部会自动
设置值,如果你不想这样,也可以自行命名,稍后通过OnInitUiListener接口进行相关文案设置;
### 2、注入到UpdateAppUtils(必须)
通过设置uiConfig,将自定义布局注入到UpdateAppUtils;注意uiType必须为UiType.CUSTOM
```
UpdateAppUtils
.getInstance()
// ...
.uiConfig(UiConfig(uiType = UiType.CUSTOM, customLayoutId = R.layout.view_update_dialog_custom))
.update()
```
### 3、实现OnInitUiListener接口(非必须)
UpdateAppUtils 中只对上表中的4个控件进行了相关内容的填充,如果你自定义的布局中有其他控件需要进行内容填充
需要实现OnInitUiListener接口来进行操作:
```
UpdateAppUtils
.getInstance()
// ...
.setOnInitUiListener(object : OnInitUiListener {
override fun onInitUpdateUi(view: View?, updateConfig: UpdateConfig, uiConfig: UiConfig) {
view?.findViewById(R.id.tv_update_title)?.text = "版本更新啦"
view?.findViewById(R.id.tv_version_name)?.text = "V2.0.0"
// do more...
}
})
```
================================================
FILE: settings.gradle
================================================
include ':app', ':updateapputils'
================================================
FILE: updateapputils/.gitignore
================================================
/build
================================================
FILE: updateapputils/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android'
apply plugin: 'com.novoda.bintray-release'
android {
compileSdkVersion 29
defaultConfig {
minSdkVersion 19
targetSdkVersion 29
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// 忽略错误信息
lintOptions {
abortOnError false
}
androidExtensions {
experimental = true
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.liulishuo.filedownloader:library:1.7.7'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
}
repositories {
mavenCentral()
}
publish {
userOrg = 'teprinciple'
groupId = 'com.teprinciple'
artifactId = 'updateapputils'
publishVersion = '2.3.0'
desc = 'A Simple library for Android update app'
website = 'https://github.com/teprinciple/UpdateAppUtils'
}
================================================
FILE: updateapputils/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/teprinciple/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: updateapputils/src/androidTest/java/teprinciple/library/ExampleInstrumentedTest.java
================================================
package teprinciple.library;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumentation test, which will execute on an Android device.
*
* @see Testing documentation
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("teprinciple.library.test", appContext.getPackageName());
}
}
================================================
FILE: updateapputils/src/main/AndroidManifest.xml
================================================
================================================
FILE: updateapputils/src/main/java/constant/DownLoadBy.kt
================================================
package constant
/**
* desc: 下载方式
* time: 2019/6/18
* @author yk
*/
object DownLoadBy {
/**
* app下载
*/
const val APP = 0x101
/**
* 浏览器下载
*/
const val BROWSER = 0x102
}
================================================
FILE: updateapputils/src/main/java/constant/UiType.kt
================================================
package constant
/**
* desc: UI 类型
* time: 2019/6/27
* @author yk
*/
object UiType {
/**
* 简洁版
*/
const val SIMPLE = "SIMPLE"
/**
* 丰富版
*/
const val PLENTIFUL = "PLENTIFUL"
/**
* 全自定义
*/
const val CUSTOM = "CUSTOM"
}
================================================
FILE: updateapputils/src/main/java/extension/BooleanKtx.kt
================================================
package extension
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
@UseExperimental(ExperimentalContracts::class)
inline fun Boolean?.yes(block: () -> Unit): Boolean? {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
if (this == true) block()
return this
}
@UseExperimental(ExperimentalContracts::class)
inline fun Boolean?.no(block: () -> Unit): Boolean? {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
if (this != true) block()
return this
}
================================================
FILE: updateapputils/src/main/java/extension/ContextKtx.kt
================================================
package extension
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build
import android.support.v4.content.FileProvider
import java.io.File
/**
* desc: context 相关扩展
* author: teprinciple on 2020/3/27.
*/
/**
* appName
*/
val Context.appName
get() = packageManager.getPackageInfo(packageName, 0)?.applicationInfo?.loadLabel(packageManager).toString()
/**
* 检测wifi是否连接
*/
fun Context.isWifiConnected(): Boolean {
val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
cm ?: return false
val networkInfo = cm.activeNetworkInfo
return networkInfo != null && networkInfo.type == ConnectivityManager.TYPE_WIFI
}
/**
* 跳转安装
*/
fun Context.installApk(apkPath: String?) {
if (apkPath.isNullOrEmpty())return
val intent = Intent(Intent.ACTION_VIEW)
val apkFile = File(apkPath)
// android 7.0 fileprovider 适配
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val contentUri = FileProvider.getUriForFile(this, this.packageName + ".fileprovider", apkFile)
intent.setDataAndType(contentUri, "application/vnd.android.package-archive")
} else {
intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive")
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
this.startActivity(intent)
}
================================================
FILE: updateapputils/src/main/java/extension/CoreKtx.kt
================================================
package extension
import android.app.ActivityManager
import android.content.Context
import android.os.Build
import android.support.v4.content.ContextCompat
import android.util.Log
import android.view.View
import update.UpdateAppUtils
import util.GlobalContextProvider
import kotlin.system.exitProcess
/**
* desc: 扩展
* author: teprinc
* iple on 2020/3/27.
*/
/**
* 全局context
*/
fun globalContext() = GlobalContextProvider.mContext
/**
* 打印日志
*/
fun log(content: String?) = UpdateAppUtils.updateInfo.config.isDebug.yes {
Log.e("[UpdateAppUtils]", content ?: "")
}
/**
* 获取color
*/
fun color(color: Int) = if (globalContext() == null) 0 else ContextCompat.getColor(globalContext()!!, color)
/**
* 获取 String
*/
fun string(string: Int) = globalContext()?.getString(string) ?: ""
/**
* view 显示隐藏
*/
fun View.visibleOrGone(show: Boolean) {
if (show) {
this.visibility = View.VISIBLE
} else {
this.visibility = View.GONE
}
}
/**
* 退出app
*/
fun exitApp() {
val manager = globalContext()!!.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
manager.appTasks.forEach { it.finishAndRemoveTask() }
} else {
exitProcess(0)
}
}
================================================
FILE: updateapputils/src/main/java/extension/StringKtx.kt
================================================
package extension
import java.io.File
/**
* desc: string 相关扩展
* author: teprinciple on 2020/3/27.
*/
/**
* 根据文件路径删除文件
*/
fun String?.deleteFile() {
kotlin.runCatching {
val file = File(this ?: "")
(file.isFile).yes {
file.delete()
log("删除成功")
}
}.onFailure {
log(it.message)
}
}
================================================
FILE: updateapputils/src/main/java/listener/Md5CheckResultListener.kt
================================================
package listener
/**
* desc: Md5校验结果回调
* time: 2019/6/21
* @author teprinciple
*/
interface Md5CheckResultListener {
fun onResult(result: Boolean)
}
================================================
FILE: updateapputils/src/main/java/listener/OnBtnClickListener.kt
================================================
package listener
/**
* desc: 按钮点击监听
* time: 2019/9/16
* @author teprinciple
*/
interface OnBtnClickListener {
/**
* 按钮点击
* @return 是否消费事件
*/
fun onClick(): Boolean
}
================================================
FILE: updateapputils/src/main/java/listener/OnInitUiListener.kt
================================================
package listener
import android.view.View
import model.UiConfig
import model.UpdateConfig
/**
* desc: 初始化UI 回调 用于进一步自定义UI
* time: 2019/6/28
* @author teprinciple
*/
interface OnInitUiListener {
/**
* 初始化更新弹窗回调
* @param view 弹窗view
* @param updateConfig 当前更新配置
* @param uiConfig 当前ui配置
*/
fun onInitUpdateUi(view: View?, updateConfig: UpdateConfig, uiConfig: UiConfig)
}
================================================
FILE: updateapputils/src/main/java/listener/UpdateDownloadListener.kt
================================================
package listener
/**
* desc: 下载监听
* time: 2019/6/19
* @author teprinciple
*/
interface UpdateDownloadListener {
/**
* 开始下载
*/
fun onStart()
/**
* 下载中
* @param progress 进度 0 - 100
*/
fun onDownload(progress: Int)
/**
* 下载完成
*/
fun onFinish()
/**
* 下载错误
*/
fun onError(e: Throwable)
}
================================================
FILE: updateapputils/src/main/java/model/UiConfig.kt
================================================
package model
import com.teprinciple.updateapputils.R
import constant.UiType
import extension.string
/**
* desc: UiConfig UI 配置
* time: 2019/6/27
* @author teprinciple
*/
data class UiConfig(
// ui类型,默认简洁版
var uiType: String = UiType.SIMPLE,
// 自定义UI 布局id
var customLayoutId: Int? = null,
// 更新弹窗中的logo
var updateLogoImgRes: Int? = null,
// 标题相关设置
var titleTextSize: Float? = null,
var titleTextColor: Int? = null,
// 更新内容相关设置
var contentTextSize: Float? = null,
var contentTextColor: Int? = null,
// 更新按钮相关设置
var updateBtnBgColor: Int? = null,
var updateBtnBgRes: Int? = null,
var updateBtnTextColor: Int? = null,
var updateBtnTextSize: Float? = null,
var updateBtnText: CharSequence = string(R.string.update_now),
// 取消按钮相关设置
var cancelBtnBgColor: Int? = null,
var cancelBtnBgRes: Int? = null,
var cancelBtnTextColor: Int? = null,
var cancelBtnTextSize: Float? = null,
var cancelBtnText: CharSequence = string(R.string.update_cancel),
// 开始下载时的Toast提示文字
var downloadingToastText: CharSequence = string(R.string.toast_download_apk),
// 下载中 下载按钮以及通知栏标题前缀,进度自动拼接在后面
var downloadingBtnText: CharSequence = string(R.string.downloading),
// 下载出错时,下载按钮及通知栏标题
var downloadFailText: CharSequence = string(R.string.download_fail)
)
================================================
FILE: updateapputils/src/main/java/model/UpdateConfig.kt
================================================
package model
import constant.DownLoadBy
data class UpdateConfig(
var isDebug: Boolean = true, // 是否是调试模式,调试模式会输出日志
var alwaysShow: Boolean = true, // 非强制更新时,是否每次都显示弹窗,用VersionName来判断
var thisTimeShow: Boolean = false, // 非强制更新时,指定本次显示弹窗
var alwaysShowDownLoadDialog: Boolean = false, // 非强制更新时,也显示下载进度dialog
var force: Boolean = false, // 是否强制更新
var apkSavePath: String = "", // apk下载存放位置
var apkSaveName: String = "", // apk 保存名(默认是app的名字)
var downloadBy: Int = DownLoadBy.APP, // 下载方式:默认app下载
//var downloadDirect: Boolean = false, // 不需要弹窗,直接开始下载安装
var checkWifi: Boolean = false, // 是否检查是否wifi
var isShowNotification: Boolean = true, // 是否在通知栏显示
var notifyImgRes: Int = 0, // 通知栏图标
var needCheckMd5: Boolean = false, // 是否需要进行md5校验,仅app下载方式有效
var showDownloadingToast: Boolean = true, // 是否需要显示 【更新下载中】文案
var serverVersionName: String = "", // 服务器上版本名
var serverVersionCode: Int = 0 // 服务器上版本号
)
================================================
FILE: updateapputils/src/main/java/model/UpdateInfo.kt
================================================
package model
import com.teprinciple.updateapputils.R
import extension.string
/**
* desc: UpdateInfo
* time: 2019/6/18
* @author teprinciple
*/
internal data class UpdateInfo(
// 更新标题
var updateTitle: CharSequence = string(R.string.update_title),
// 更新内容
var updateContent: CharSequence = string(R.string.update_content),
// apk 下载地址
var apkUrl: String = "",
// 更新配置
var config: UpdateConfig = UpdateConfig(),
// ui配置
var uiConfig: UiConfig = UiConfig()
)
================================================
FILE: updateapputils/src/main/java/ui/UpdateAppActivity.kt
================================================
package ui
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.view.MotionEvent
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import com.teprinciple.updateapputils.R
import constant.DownLoadBy
import constant.UiType
import extension.*
import update.DownloadAppUtils
import update.UpdateAppService
import update.UpdateAppUtils
import util.AlertDialogUtil
import util.GlobalContextProvider
import util.SPUtil
/**
* desc: 更新弹窗
* author: teprinciple on 2019/06/3.
*/
internal class UpdateAppActivity : AppCompatActivity() {
private var tvTitle: TextView? = null
private var tvContent: TextView? = null
private var sureBtn: View? = null
private var cancelBtn: View? = null
private var ivLogo: ImageView? = null
/**
* 更新信息
*/
private val updateInfo by lazy { UpdateAppUtils.updateInfo }
/**
* 更新配置
*/
private val updateConfig by lazy { updateInfo.config }
/**
* ui 配置
*/
private val uiConfig by lazy { updateInfo.uiConfig }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (GlobalContextProvider.mContext == null){
GlobalContextProvider.mContext = this.applicationContext
}
setContentView(
when (uiConfig.uiType) {
UiType.SIMPLE -> R.layout.view_update_dialog_simple
UiType.PLENTIFUL -> R.layout.view_update_dialog_plentiful
UiType.CUSTOM -> uiConfig.customLayoutId ?: R.layout.view_update_dialog_simple
else -> R.layout.view_update_dialog_simple
}
)
initView()
initUi()
// 初始化UI回调,用于进一步自定义UI
UpdateAppUtils.onInitUiListener?.onInitUpdateUi(
window.decorView.findViewById(android.R.id.content),
updateConfig,
uiConfig)
// 每次弹窗后,下载前均把本地之前缓存的apk删除,避免缓存老版本apk或者问题apk,并不重新下载新的apk
SPUtil.getString(DownloadAppUtils.KEY_OF_SP_APK_PATH, "").deleteFile()
}
@SuppressLint("ClickableViewAccessibility")
private fun initView() {
tvTitle = findViewById(R.id.tv_update_title)
tvContent = findViewById(R.id.tv_update_content)
cancelBtn = findViewById(R.id.btn_update_cancel)
sureBtn = findViewById(R.id.btn_update_sure)
ivLogo = findViewById(R.id.iv_update_logo)
// 更新标题
tvTitle?.text = updateInfo.updateTitle
// 更新内容
tvContent?.text = updateInfo.updateContent
// 取消
cancelBtn?.setOnClickListener {
updateConfig.force.yes {
exitApp()
}.no {
finish()
}
}
// 确定
sureBtn?.setOnClickListener {
DownloadAppUtils.isDownloading.no {
if (sureBtn is TextView) {
(sureBtn as? TextView)?.text = uiConfig.updateBtnText
}
preDownLoad()
}
}
// 显示或隐藏取消按钮, 强更时默认不显示取消按钮
hideShowCancelBtn(!updateConfig.force)
// 外部额外设置 取消 按钮点击事件
cancelBtn?.setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_UP -> {
UpdateAppUtils.onCancelBtnClickListener?.onClick() ?: false
}
else -> false
}
}
// 外部额外设置 立即更新 按钮点击事件
sureBtn?.setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_UP -> {
UpdateAppUtils.onUpdateBtnClickListener?.onClick() ?: false
}
else -> false
}
}
}
/**
* 取消按钮处理
*/
private fun hideShowCancelBtn(show: Boolean) {
// 强制更新 不显示取消按钮
cancelBtn?.visibleOrGone(show)
// 取消按钮与确定按钮中的间隔线
findViewById(R.id.view_line)?.visibleOrGone(show)
}
/**
* 初始化UI
*/
private fun initUi() {
uiConfig.apply {
// 设置更新logo
updateLogoImgRes?.let { ivLogo?.setImageResource(it) }
// 设置标题字体颜色、大小
titleTextColor?.let { tvTitle?.setTextColor(it) }
titleTextSize?.let { tvTitle?.setTextSize(it) }
// 设置标题字体颜色、大小
contentTextColor?.let { tvContent?.setTextColor(it) }
contentTextSize?.let { tvContent?.setTextSize(it) }
// 更新按钮相关设置
updateBtnBgColor?.let { sureBtn?.setBackgroundColor(it) }
updateBtnBgRes?.let { sureBtn?.setBackgroundResource(it) }
if (sureBtn is TextView) {
updateBtnTextColor?.let { (sureBtn as? TextView)?.setTextColor(it) }
updateBtnTextSize?.let { (sureBtn as? TextView)?.setTextSize(it) }
(sureBtn as? TextView)?.text = updateBtnText
}
// 取消按钮相关设置
cancelBtnBgColor?.let { cancelBtn?.setBackgroundColor(it) }
cancelBtnBgRes?.let { cancelBtn?.setBackgroundResource(it) }
if (cancelBtn is TextView) {
cancelBtnTextColor?.let { (cancelBtn as? TextView)?.setTextColor(it) }
cancelBtnTextSize?.let { (cancelBtn as? TextView)?.setTextSize(it) }
(cancelBtn as? TextView)?.text = cancelBtnText
}
}
}
override fun onBackPressed() {
// do noting 禁用返回键
}
/**
* 预备下载 进行 6.0权限检查
*/
private fun preDownLoad() {
// 6.0 以下不用动态权限申请
(Build.VERSION.SDK_INT < Build.VERSION_CODES.M ).yes {
download()
}.no {
val writePermission = ContextCompat.checkSelfPermission(this, permission)
(writePermission == PackageManager.PERMISSION_GRANTED).yes {
download()
}.no {
// 申请权限
ActivityCompat.requestPermissions(this, arrayOf(permission), PERMISSION_CODE)
}
}
}
/**
* 下载判断
*/
private fun download() {
// 动态注册广播,8.0 静态注册收不到
// 开启服务注册,避免直接在Activity中注册广播生命周期随Activity终止而终止
startService(Intent(this, UpdateAppService::class.java))
when (updateConfig.downloadBy) {
// App下载
DownLoadBy.APP -> {
(updateConfig.checkWifi && !isWifiConnected()).yes {
// 需要进行WiFi判断
AlertDialogUtil.show(this, getString(R.string.check_wifi_notice), onSureClick = {
realDownload()
})
}.no {
// 不需要wifi判断,直接下载
realDownload()
}
}
// 浏览器下载
DownLoadBy.BROWSER -> {
DownloadAppUtils.downloadForWebView(updateInfo.apkUrl)
}
}
}
/**
* 实际下载
*/
@SuppressLint("SetTextI18n")
private fun realDownload() {
if ((updateConfig.force || updateConfig.alwaysShowDownLoadDialog) && sureBtn is TextView) {
DownloadAppUtils.onError = {
(sureBtn as? TextView)?.text = uiConfig.downloadFailText
(updateConfig.alwaysShowDownLoadDialog).yes {
hideShowCancelBtn(true)
}
}
DownloadAppUtils.onReDownload = {
(sureBtn as? TextView)?.text = uiConfig.updateBtnText
}
DownloadAppUtils.onProgress = {
(it == 100).yes {
(sureBtn as? TextView)?.text = getString(R.string.install)
(updateConfig.alwaysShowDownLoadDialog).yes {
hideShowCancelBtn(true)
}
}.no {
(sureBtn as? TextView)?.text = "${uiConfig.downloadingBtnText}$it%"
(updateConfig.alwaysShowDownLoadDialog).yes {
hideShowCancelBtn(false)
}
}
}
}
DownloadAppUtils.download()
(updateConfig.showDownloadingToast).yes {
Toast.makeText(this, uiConfig.downloadingToastText, Toast.LENGTH_SHORT).show()
}
// 非强制安装且alwaysShowDownLoadDialog为false时,开始下载后取消弹窗
(!updateConfig.force && !updateConfig.alwaysShowDownLoadDialog).yes {
finish()
}
}
/**
* 权限请求结果
*/
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
PERMISSION_CODE -> (grantResults.getOrNull(0) == PackageManager.PERMISSION_GRANTED).yes {
download()
}.no {
ActivityCompat.shouldShowRequestPermissionRationale(this, permission).no {
// 显示无权限弹窗
AlertDialogUtil.show(this, getString(R.string.no_storage_permission), onSureClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.parse("package:$packageName") // 根据包名打开对应的设置界面
startActivity(intent)
})
}
}
}
}
override fun finish() {
super.finish()
overridePendingTransition(0, 0)
}
companion object {
fun launch() = globalContext()?.let {
val intent = Intent(it, UpdateAppActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
it.startActivity(intent)
}
private const val permission = Manifest.permission.WRITE_EXTERNAL_STORAGE
private const val PERMISSION_CODE = 1001
}
}
================================================
FILE: updateapputils/src/main/java/update/DownloadAppUtils.kt
================================================
package update
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import com.liulishuo.filedownloader.BaseDownloadTask
import com.liulishuo.filedownloader.FileDownloadLargeFileListener
import com.liulishuo.filedownloader.FileDownloader
import extension.*
import util.FileDownloadUtil
import util.SPUtil
import util.SignMd5Util
import java.io.File
/**
* Created by Teprinciple on 2016/12/13.
*/
internal object DownloadAppUtils {
const val KEY_OF_SP_APK_PATH = "KEY_OF_SP_APK_PATH"
/**
* apk 下载后本地文件路径
*/
var downloadUpdateApkFilePath: String = ""
/**
* 更新信息
*/
private val updateInfo by lazy { UpdateAppUtils.updateInfo }
/**
* context
*/
private val context by lazy { globalContext()!! }
/**
* 是否在下载中
*/
var isDownloading = false
/**
*下载进度回调
*/
var onProgress: (Int) -> Unit = {}
/**
* 下载出错回调
*/
var onError: () -> Unit = {}
/**
* 出错,点击重试回调
*/
var onReDownload: () -> Unit = {}
/**
* 通过浏览器下载APK包
*/
fun downloadForWebView(url: String) {
val uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, uri)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
/**
* 出错后,点击重试
*/
fun reDownload() {
onReDownload.invoke()
download()
}
/**
* App下载APK包,下载完成后安装
*/
fun download() {
(Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED).no {
log("没有SD卡")
onError.invoke()
return
}
var filePath = ""
(updateInfo.config.apkSavePath.isNotEmpty()).yes {
filePath = updateInfo.config.apkSavePath
}.no {
// 适配Android10
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !Environment.isExternalStorageLegacy()){
filePath = (context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath ?: "") + "/apk"
}else{
val packageName = context.packageName
filePath = Environment.getExternalStorageDirectory().absolutePath + "/" + packageName
}
}
// apk 保存名称
val apkName = if (updateInfo.config.apkSaveName.isNotEmpty()) {
updateInfo.config.apkSaveName
} else {
context.appName
}
val apkLocalPath = "$filePath/$apkName.apk"
downloadUpdateApkFilePath = apkLocalPath
SPUtil.putBase(KEY_OF_SP_APK_PATH, downloadUpdateApkFilePath)
FileDownloader.setup(context)
val downloadTask = FileDownloader.getImpl().create(updateInfo.apkUrl)
.setPath(apkLocalPath)
downloadTask
.addHeader("Accept-Encoding","identity")
.addHeader("User-Agent","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36")
.setListener(object : FileDownloadLargeFileListener() {
override fun pending(task: BaseDownloadTask, soFarBytes: Long, totalBytes: Long) {
log("----使用FileDownloader下载-------")
log("pending:soFarBytes($soFarBytes),totalBytes($totalBytes)")
downloadStart()
if(totalBytes < 0){
downloadTask.pause()
}
}
override fun progress(task: BaseDownloadTask, soFarBytes: Long, totalBytes: Long) {
downloading(soFarBytes, totalBytes)
if(totalBytes < 0){
downloadTask.pause()
}
}
override fun paused(task: BaseDownloadTask, soFarBytes: Long, totalBytes: Long) {
log("获取文件总长度失败出错,尝试HTTPURLConnection下载")
downloadUpdateApkFilePath.deleteFile()
"$downloadUpdateApkFilePath.temp".deleteFile()
downloadByHttpUrlConnection(filePath, apkName)
}
override fun completed(task: BaseDownloadTask) {
downloadComplete()
}
override fun error(task: BaseDownloadTask, e: Throwable) {
// FileDownloader 下载失败后,再调用 FileDownloadUtil 下载一次
// FileDownloader 对码云或者阿里云上的apk文件会下载失败
// downloadError(e)
log("下载出错,尝试HTTPURLConnection下载")
downloadUpdateApkFilePath.deleteFile()
"$downloadUpdateApkFilePath.temp".deleteFile()
downloadByHttpUrlConnection(filePath, apkName)
}
override fun warn(task: BaseDownloadTask) {
}
}).start()
}
/**
* 使用 HttpUrlConnection 下载
*/
private fun downloadByHttpUrlConnection(filePath: String, apkName: String?) {
FileDownloadUtil.download(
updateInfo.apkUrl,
filePath,
"$apkName.apk",
onStart = { downloadStart() },
onProgress = { current, total -> downloading(current, total) },
onComplete = { downloadComplete() },
onError = { downloadError(it) }
)
}
/**
* 开始下载逻辑
*/
private fun downloadStart() {
isDownloading = true
UpdateAppUtils.downloadListener?.onStart()
UpdateAppReceiver.send(context, 0)
}
/**
* 下载中逻辑
*/
private fun downloading(soFarBytes: Long, totalBytes: Long) {
// log("soFarBytes:$soFarBytes--totalBytes:$totalBytes")
isDownloading = true
var progress = (soFarBytes * 100.0 / totalBytes).toInt()
if (progress < 0) progress = 0
log("progress:$progress")
UpdateAppReceiver.send(context, progress)
this@DownloadAppUtils.onProgress.invoke(progress)
UpdateAppUtils.downloadListener?.onDownload(progress)
}
/**
* 下载完成处理逻辑
*/
private fun downloadComplete() {
isDownloading = false
log("completed")
this@DownloadAppUtils.onProgress.invoke(100)
UpdateAppUtils.downloadListener?.onFinish()
// 校验md5
(updateInfo.config.needCheckMd5).yes {
checkMd5(context)
}.no {
UpdateAppReceiver.send(context, 100)
}
}
/**
* 下载失败处理逻辑
*/
private fun downloadError(e: Throwable) {
isDownloading = false
log("error:${e.message}")
downloadUpdateApkFilePath.deleteFile()
this@DownloadAppUtils.onError.invoke()
UpdateAppUtils.downloadListener?.onError(e)
UpdateAppReceiver.send(context, -1000)
}
/**
* 校验Md5
* 先获取本应用的MD5值,获取未安装应用的MD5.进行对比
*/
private fun checkMd5(context: Context) {
// 当前应用md5
val localMd5 = SignMd5Util.getAppSignatureMD5()
// 下载的apk 签名md5
val apkMd5 = SignMd5Util.getSignMD5FromApk(File(downloadUpdateApkFilePath))
log("当前应用签名md5:$localMd5")
log("下载apk签名md5:$apkMd5")
// 校验结果回调
UpdateAppUtils.md5CheckResultListener?.onResult(localMd5.equals(apkMd5, true))
(localMd5.equals(apkMd5, true)).yes {
log("md5校验成功")
UpdateAppReceiver.send(context, 100)
}.no {
log("md5校验失败")
}
}
}
================================================
FILE: updateapputils/src/main/java/update/UpdateAppReceiver.kt
================================================
package update
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Build
import extension.installApk
import extension.no
import extension.yes
/**
* desc: UpdateAppReceiver
* author: teprinciple on 2019/06/3.
*/
internal class UpdateAppReceiver : BroadcastReceiver() {
private val notificationChannel = "1001"
private val updateConfig by lazy { UpdateAppUtils.updateInfo.config }
private val uiConfig by lazy { UpdateAppUtils.updateInfo.uiConfig }
private var lastProgress = 0
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
// 下载中
context.packageName + ACTION_UPDATE -> {
// 进度
val progress = intent.getIntExtra(KEY_OF_INTENT_PROGRESS, 0)
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
(progress != -1000).yes {
lastProgress = progress
}
// 显示通知栏
val notifyId = 1
updateConfig.isShowNotification.yes {
showNotification(context, notifyId, progress, notificationChannel, nm)
}
// 下载完成
if (progress == 100) {
handleDownloadComplete(context, notifyId, nm)
}
}
// 重新下载
context.packageName + ACTION_RE_DOWNLOAD -> {
DownloadAppUtils.reDownload()
}
}
}
/**
* 下载完成后的逻辑
*/
private fun handleDownloadComplete(context: Context, notifyId: Int, nm: NotificationManager?) {
// 关闭通知栏
nm?.let {
nm.cancel(notifyId)
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
nm.deleteNotificationChannel(notificationChannel)
}
}
// 安装apk
context.installApk(DownloadAppUtils.downloadUpdateApkFilePath)
}
/**
* 通知栏显示
*/
private fun showNotification(context: Context, notifyId: Int, progress: Int, notificationChannel: String, nm: NotificationManager) {
val notificationName = "notification"
// 适配 8.0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 通知渠道
val channel = NotificationChannel(notificationChannel, notificationName, NotificationManager.IMPORTANCE_HIGH)
channel.enableLights(false)
// 是否在桌面icon右上角展示小红点
channel.setShowBadge(false)
// 是否在久按桌面图标时显示此渠道的通知
channel.enableVibration(false)
// 最后在notificationmanager中创建该通知渠道
nm.createNotificationChannel(channel)
}
val builder = Notification.Builder(context)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(notificationChannel)
}
// 设置通知图标
(updateConfig.notifyImgRes > 0).yes {
builder.setSmallIcon(updateConfig.notifyImgRes)
builder.setLargeIcon(BitmapFactory.decodeResource(context.resources, updateConfig.notifyImgRes))
}.no {
builder.setSmallIcon(android.R.mipmap.sym_def_app_icon)
}
// 设置进度
builder.setProgress(100, lastProgress, false)
if (progress == -1000) {
val intent = Intent(context.packageName + ACTION_RE_DOWNLOAD)
intent.setPackage(context.packageName)
val pendingIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_CANCEL_CURRENT)
builder.setContentIntent(pendingIntent)
// 通知栏标题
builder.setContentTitle(uiConfig.downloadFailText)
} else {
// 通知栏标题
builder.setContentTitle("${uiConfig.downloadingBtnText}$progress%")
}
// 设置只响一次
builder.setOnlyAlertOnce(true)
val notification = builder.build()
nm.notify(notifyId, notification)
}
companion object {
/**
* 进度key
*/
private const val KEY_OF_INTENT_PROGRESS = "KEY_OF_INTENT_PROGRESS"
/**
* ACTION_UPDATE
*/
const val ACTION_UPDATE = "teprinciple.update"
/**
* ACTION_RE_DOWNLOAD
*/
const val ACTION_RE_DOWNLOAD = "action_re_download"
const val REQUEST_CODE = 1001
/**
* 发送进度通知
*/
fun send(context: Context, progress: Int) {
val intent = Intent(context.packageName + ACTION_UPDATE)
intent.putExtra(KEY_OF_INTENT_PROGRESS, progress)
context.sendBroadcast(intent)
}
}
}
================================================
FILE: updateapputils/src/main/java/update/UpdateAppService.kt
================================================
package update
import android.app.Service
import android.content.Intent
import android.content.IntentFilter
import android.os.IBinder
/**
* desc: UpdateAppService
* author: teprinciple on 2018/11/3.
*/
internal class UpdateAppService : Service() {
private val updateAppReceiver = UpdateAppReceiver()
override fun onCreate() {
super.onCreate()
// 动态注册receiver 适配8.0 updateAppReceiver 静态注册没收不到广播
registerReceiver(updateAppReceiver, IntentFilter(packageName + UpdateAppReceiver.ACTION_UPDATE))
registerReceiver(updateAppReceiver, IntentFilter(packageName + UpdateAppReceiver.ACTION_RE_DOWNLOAD))
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(updateAppReceiver) // 注销广播
}
override fun onBind(intent: Intent): IBinder? {
return null
}
}
================================================
FILE: updateapputils/src/main/java/update/UpdateAppUtils.kt
================================================
package update
import android.content.Context
import extension.globalContext
import extension.log
import extension.no
import extension.yes
import listener.OnBtnClickListener
import listener.Md5CheckResultListener
import listener.OnInitUiListener
import listener.UpdateDownloadListener
import model.UiConfig
import model.UpdateConfig
import model.UpdateInfo
import ui.UpdateAppActivity
import util.GlobalContextProvider
import util.SPUtil
/**
* Created by Teprinciple on 2016/11/15.
*/
object UpdateAppUtils {
// 更新信息对象
internal val updateInfo by lazy { UpdateInfo() }
// 下载监听
internal var downloadListener: UpdateDownloadListener? = null
// md5校验结果回调
internal var md5CheckResultListener: Md5CheckResultListener? = null
// 初始化更新弹窗UI回调
internal var onInitUiListener: OnInitUiListener? = null
// "暂不更新"按钮点击事件
internal var onCancelBtnClickListener: OnBtnClickListener? = null
// "立即更新"按钮点击事件
internal var onUpdateBtnClickListener: OnBtnClickListener? = null
/**
* 设置apk下载地址
*/
fun apkUrl(apkUrl: String): UpdateAppUtils {
updateInfo.apkUrl = apkUrl
return this
}
/**
* 设置更新标题
*/
fun updateTitle(title: CharSequence): UpdateAppUtils {
updateInfo.updateTitle = title
return this
}
/**
* 设置更新内容
*/
fun updateContent(content: CharSequence): UpdateAppUtils {
updateInfo.updateContent = content
return this
}
/**
* 设置更新配置
*/
fun updateConfig(config: UpdateConfig): UpdateAppUtils {
updateInfo.config = config
return this
}
/**
* 设置UI配置
*/
fun uiConfig(uiConfig: UiConfig): UpdateAppUtils {
updateInfo.uiConfig = uiConfig
return this
}
/**
* 设置下载监听
*/
fun setUpdateDownloadListener(listener: UpdateDownloadListener?): UpdateAppUtils {
this.downloadListener = listener
return this
}
/**
* 设置md5校验结果监听
*/
fun setMd5CheckResultListener(listener: Md5CheckResultListener?): UpdateAppUtils {
this.md5CheckResultListener = listener
return this
}
/**
* 设置初始化UI监听
*/
fun setOnInitUiListener(listener: OnInitUiListener?): UpdateAppUtils {
this.onInitUiListener = listener
return this
}
/**
* 设置 “暂不更新” 按钮点击事件
*/
fun setCancelBtnClickListener(listener: OnBtnClickListener?): UpdateAppUtils {
this.onCancelBtnClickListener = listener
return this
}
/**
* 设置 “立即更新” 按钮点击事件
*/
fun setUpdateBtnClickListener(listener: OnBtnClickListener?): UpdateAppUtils {
this.onUpdateBtnClickListener = listener
return this
}
/**
* 检查更新
*/
fun update() {
if(globalContext() == null){
log("请先调用初始化init")
return
}
val keyName = (globalContext()?.packageName ?: "") + updateInfo.config.serverVersionName
// 设置每次显示,设置本次显示及强制更新 每次都显示弹窗
(updateInfo.config.alwaysShow || updateInfo.config.thisTimeShow || updateInfo.config.force).yes {
UpdateAppActivity.launch()
}.no {
val hasShow = SPUtil.getBoolean(keyName, false)
(hasShow).no { UpdateAppActivity.launch() }
}
SPUtil.putBase(keyName, true)
}
/* 未缓存apk
/**
* 删除已安装 apk
*/
fun deleteInstalledApk() {
val apkPath = SPUtil.getString(DownloadAppUtils.KEY_OF_SP_APK_PATH, "")
val appVersionCode = Utils.getAPPVersionCode()
val apkVersionCode = Utils.getApkVersionCode(apkPath)
log("appVersionCode:$appVersionCode")
log("apkVersionCode:$apkVersionCode")
(apkPath.isNotEmpty() && appVersionCode == apkVersionCode && apkVersionCode > 0).yes {
Utils.deleteFile(apkPath)
}
}
*/
/**
* 获取单例对象
*/
@JvmStatic
fun getInstance() = this
/**
* 初始化,非必须。解决部分手机 通过UpdateFileProvider 获取不到context情况使用
* * @param context 提供全局context。
*/
@JvmStatic
fun init(context: Context){
GlobalContextProvider.mContext = context.applicationContext
log("外部初始化context")
}
}
================================================
FILE: updateapputils/src/main/java/update/UpdateFileProvider.kt
================================================
package update
import android.support.v4.content.FileProvider
import extension.log
import extension.yes
import util.GlobalContextProvider
/**
* desc: UpdateFileProvider
* time: 2019/7/10
* @author Teprinciple
*/
class UpdateFileProvider : FileProvider() {
override fun onCreate(): Boolean {
val result = super.onCreate()
(GlobalContextProvider.mContext == null && context != null).yes {
GlobalContextProvider.mContext = context
log("内部Provider初始化context:" + GlobalContextProvider.mContext)
}
return result
}
}
================================================
FILE: updateapputils/src/main/java/util/AlertDialogUtil.kt
================================================
package util
import android.app.Activity
import android.app.AlertDialog
import com.teprinciple.updateapputils.R
import extension.string
/**
* desc: AlertDialogUtil
* time: 2018/8/20
* @author teprinciple
*/
internal object AlertDialogUtil {
fun show(
activity: Activity,
message: String,
onCancelClick: () -> Unit = {},
onSureClick: () -> Unit = {},
cancelable: Boolean = false,
title: String = string(R.string.notice),
cancelText: String = string(R.string.cancel),
sureText: String = string(R.string.sure)
) {
AlertDialog.Builder(activity, R.style.AlertDialog)
.setTitle(title)
.setMessage(message)
.setPositiveButton(sureText) { _, _ ->
onSureClick.invoke()
}
.setNegativeButton(cancelText) { _, _ ->
onCancelClick.invoke()
}
.setCancelable(cancelable)
.create()
.show()
}
}
================================================
FILE: updateapputils/src/main/java/util/FileDownloadUtil.kt
================================================
package util
import extension.log
import extension.no
import extension.yes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.net.HttpURLConnection
import java.net.HttpURLConnection.HTTP_OK
import java.net.URL
/**
* desc: 文件下载 当 FileDownloader 对某些apk下载失败时(比如:放在阿里云,码云上apk) 使用该工具类下载
* time: 2019/8/28
* @author teprinciple
*/
internal object FileDownloadUtil {
/**
* 下载文件
* @param url 文件地址
* @param fileSavePath 文件存储地址
* @param fileName 文件存储名称
* @param onStart 开始下载回调
* @param onProgress 下载中回调
* @param onComplete 下载完成回调
* @param onError 下载失败回调
*/
fun download(
url: String,
fileSavePath: String,
fileName: String?,
onStart: () -> Unit = {},
onProgress: (current: Long, total: Long) -> Unit = { _, _ -> },
onComplete: () -> Unit = {},
onError: (Throwable) -> Unit = {}
) {
GlobalScope.launch(Dispatchers.IO) {
log("----使用HttpURLConnection下载----")
onStart.invoke()
var connection: HttpURLConnection? = null
var outputStream: FileOutputStream? = null
kotlin.runCatching {
connection = URL(url).openConnection() as HttpURLConnection
outputStream = FileOutputStream(File(fileSavePath, fileName))
connection?.apply {
requestMethod = "GET"
setRequestProperty("Charset", "utf-8")
setRequestProperty("Accept-Encoding", "identity")
setRequestProperty("User-Agent", " Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36")
connect()
}
val responseCode = connection!!.responseCode
if (responseCode == HTTP_OK) {
val total = connection!!.contentLength
var progress = -1
connection!!.inputStream.use { input ->
outputStream.use { output ->
input.copyToWithProgress(output!!) {
val pro = (it * 100.0 / total).toInt()
(progress != pro).yes {
GlobalScope.launch(Dispatchers.Main) {
onProgress(it, total.toLong())
}
}
progress = pro
}
}
}
}else{
throw Throwable(message = "文件下载错误")
}
}.onSuccess {
connection?.disconnect()
outputStream?.close()
log("HttpURLConnection下载完成")
GlobalScope.launch(Dispatchers.Main) {
(File(fileSavePath).length() > 0L).yes{
onComplete.invoke()
}.no {
onError.invoke(Throwable(message = "文件下载错误"))
}
}
}.onFailure {
connection?.disconnect()
outputStream?.close()
log("HttpURLConnection下载失败:${it.message}")
GlobalScope.launch(Dispatchers.Main) {
onError.invoke(it)
}
}
}
}
}
fun InputStream.copyToWithProgress(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE, currentByte: (Long) -> Unit = {}): Long {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = read(buffer)
currentByte.invoke(bytesCopied)
}
return bytesCopied
}
================================================
FILE: updateapputils/src/main/java/util/GlobalContextProvider.kt
================================================
package util
import android.annotation.SuppressLint
import android.content.Context
/**
* desc: 提供context.
*/
@SuppressLint("StaticFieldLeak")
internal object GlobalContextProvider {
/** 全局context 提供扩展globalContext */
internal var mContext: Context? = null
}
================================================
FILE: updateapputils/src/main/java/util/SPUtil.kt
================================================
package util
import android.app.Activity
import android.content.SharedPreferences
import extension.globalContext
/**
* SharedPreferences 数据保存
*/
internal object SPUtil {
fun putBase(keyName: String, value: Any): Boolean? {
val sharedPreferences = getSp()
val editor: SharedPreferences.Editor? = sharedPreferences?.edit()
when (value) {
is Int -> editor?.putInt(keyName, value)
is Boolean -> editor?.putBoolean(keyName, value)
is Float -> editor?.putFloat(keyName, value)
is String -> editor?.putString(keyName, value)
is Long -> editor?.putLong(keyName, value)
else -> throw IllegalArgumentException("SharedPreferences can,t be save this type")
}
return editor?.commit()
}
fun getBoolean(keyName: String, defaultValue: Boolean = false): Boolean {
val sharedPreferences = getSp()
return sharedPreferences?.getBoolean(keyName, defaultValue) ?: false
}
fun getString(keyName: String, defaultValue: String? = null): String {
val sharedPreferences = getSp()
return sharedPreferences?.getString(keyName, defaultValue) ?: ""
}
private fun getSp(): SharedPreferences? {
if (globalContext() == null) return null
return globalContext()!!.getSharedPreferences(globalContext()!!.packageName, Activity.MODE_PRIVATE)
}
}
================================================
FILE: updateapputils/src/main/java/util/SignMd5Util.kt
================================================
package util
import android.content.pm.PackageManager
import android.content.pm.Signature
import extension.globalContext
import java.io.File
import java.io.IOException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.Certificate
import java.util.*
import java.util.jar.JarEntry
import java.util.jar.JarFile
import kotlin.experimental.and
/**
* desc: 获取签名 md5
* time: 2019/6/21
* @author teprinciple
*/
internal object SignMd5Util {
private val HEX_DIGITS = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')
/**
* 获取当前应用签名文件md5
*/
fun getAppSignatureMD5(): String {
val packageName = globalContext()?.packageName ?: ""
if (packageName.isEmpty()) return ""
val signature = getAppSignature(packageName)
return if (signature == null || signature.isEmpty()) {
""
} else {
bytes2HexString(hashTemplate(signature[0].toByteArray(), "MD5"))
.replace("(?<=[0-9A-F]{2})[0-9A-F]{2}".toRegex(), ":$0")
}
}
/**
* 获取未安装apk 签名文件md5
*/
fun getSignMD5FromApk(file: File): String {
val signatures = ArrayList()
val jarFile = JarFile(file)
try {
val je = jarFile.getJarEntry("AndroidManifest.xml")
val readBuffer = ByteArray(8192)
val certs = loadCertificates(jarFile, je, readBuffer)
if (certs != null) {
for (c in certs) {
val sig = bytes2HexString(hashTemplate(c.encoded, "MD5"))
.replace("(?<=[0-9A-F]{2})[0-9A-F]{2}".toRegex(), ":$0")
signatures.add(sig)
}
}
} catch (ex: Exception) {
}
return signatures.getOrNull(0) ?: ""
}
private fun getAppSignature(packageName: String): Array? {
if (packageName.isEmpty()) return null
return try {
val pm = globalContext()?.packageManager
val pi = pm?.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
pi?.signatures
} catch (e: Exception) {
e.printStackTrace()
null
}
}
private fun hashTemplate(data: ByteArray?, algorithm: String): ByteArray? {
if (data == null || data.isEmpty()) return null
return try {
val md = MessageDigest.getInstance(algorithm)
md.update(data)
md.digest()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
null
}
}
private fun bytes2HexString(bytes: ByteArray?): String {
if (bytes == null) return ""
val len = bytes.size
if (len <= 0) return ""
val ret = CharArray(len shl 1)
var i = 0
var j = 0
while (i < len) {
ret[j++] = HEX_DIGITS[bytes[i].toInt().shr(4) and 0x0f]
ret[j++] = HEX_DIGITS[(bytes[i] and 0x0f).toInt()]
i++
}
return String(ret)
}
/**
* 加载签名
*/
private fun loadCertificates(jarFile: JarFile, je: JarEntry?, readBuffer: ByteArray): Array? {
try {
val inputStream = jarFile.getInputStream(je)
while (inputStream.read(readBuffer, 0, readBuffer.size) != -1) {
}
inputStream.close()
return je?.certificates
} catch (e: IOException) {
}
return null
}
}
================================================
FILE: updateapputils/src/main/res/anim/dialog_enter.xml
================================================
================================================
FILE: updateapputils/src/main/res/anim/dialog_out.xml
================================================
================================================
FILE: updateapputils/src/main/res/drawable/bg_update_btn.xml
================================================
================================================
FILE: updateapputils/src/main/res/drawable/bg_update_dialog.xml
================================================
================================================
FILE: updateapputils/src/main/res/layout/view_update_dialog_plentiful.xml
================================================
================================================
FILE: updateapputils/src/main/res/layout/view_update_dialog_simple.xml
================================================
================================================
FILE: updateapputils/src/main/res/values/colors.xml
================================================
#ffffff#0076FF#333333#555555
================================================
FILE: updateapputils/src/main/res/values/strings.xml
================================================
提示取消确认立即更新暂不更新版本更新啦!发现新版本,立即更新下载出错,点击重试更新下载中...下载中"暂无储存权限,是否前往打开""当前没有连接Wifi,是否继续下载"立即安装
================================================
FILE: updateapputils/src/main/res/values/styles.xml
================================================
================================================
FILE: updateapputils/src/main/res/values-en/strings.xml
================================================
NoticeCancelOKUpdateCancelNew version!New version get ready,update nowDownload error, Click retryStart downloading...downloadingPlease allow access to storage permissionsCurrent net type is not Wifi, Whether to continueInstall
================================================
FILE: updateapputils/src/main/res/xml/network_security_config.xml
================================================
================================================
FILE: updateapputils/src/main/res/xml/update_file_paths.xml
================================================