package juloo.keyboard2; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.graphics.Canvas; import android.graphics.Insets; import android.graphics.Paint; import android.graphics.LinearGradient; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.inputmethodservice.InputMethodService; import android.net.Uri; import android.os.Build.VERSION; import android.os.Handler; import android.os.Looper; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; import java.util.Arrays; import java.util.List; import java.util.Locale; public class Keyboard2View extends View implements View.OnTouchListener, Pointers.IPointerEventHandler { private KeyboardData _keyboard; /** The key holding the shift key is used to set shift state from autocapitalisation. */ private KeyboardData.Key _shift_key; /** Used to add fake pointers. */ private KeyboardData.Key _compose_key; private Pointers _pointers; private Pointers.Modifiers _mods; /** Long-pressing the usage gauge key opens the usage page; a tap stays inert. */ private final Handler _gaugeHandler = new Handler(Looper.getMainLooper()); private Runnable _gaugeLongPress; private boolean _gaugePressed; private static int _currentWhat = 0; private Config _config; private float _keyWidth; private float _mainLabelSize; private float _subLabelSize; private float _marginRight; private float _marginLeft; private float _marginBottom; private int _insets_left = 0; private int _insets_right = 0; private int _insets_bottom = 0; private Theme _theme; private Theme.Computed _tc; private static RectF _tmpRect = new RectF(); private static final Paint GAUGE_PAINT = new Paint(Paint.ANTI_ALIAS_FLAG); private static final Path GAUGE_PATH = new Path(); enum Vertical { TOP, CENTER, BOTTOM } public Keyboard2View(Context context, AttributeSet attrs) { super(context, attrs); _theme = new Theme(getContext(), attrs); _config = Config.globalConfig(); _pointers = new Pointers(this, _config); refresh_navigation_bar(context); setOnTouchListener(this); int layout_id = (attrs == null) ? 0 : attrs.getAttributeResourceValue(null, "layout", 0); if (layout_id == 0) reset(); else setKeyboard(KeyboardData.load(getResources(), layout_id)); } private Window getParentWindow(Context context) { if (context instanceof InputMethodService) return ((InputMethodService)context).getWindow().getWindow(); if (context instanceof ContextWrapper) return getParentWindow(((ContextWrapper)context).getBaseContext()); return null; } public void refresh_navigation_bar(Context context) { if (VERSION.SDK_INT < 21) return; // The intermediate Window is a [Dialog]. Window w = getParentWindow(context); w.setNavigationBarColor(_theme.colorNavBar); if (VERSION.SDK_INT < 26) return; int uiFlags = getSystemUiVisibility(); if (_theme.isLightNavBar) uiFlags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; else uiFlags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; setSystemUiVisibility(uiFlags); } public void setKeyboard(KeyboardData kw) { _keyboard = kw; _shift_key = _keyboard.findKeyWithValue(KeyValue.SHIFT); _compose_key = _keyboard.findKeyWithValue(KeyValue.COMPOSE); KeyModifier.set_modmap(_keyboard.modmap); reset(); } public void reset() { _mods = Pointers.Modifiers.EMPTY; _pointers.clear(); requestLayout(); invalidate(); } void set_fake_ptr_latched(KeyboardData.Key key, KeyValue kv, boolean latched, boolean lock) { if (_keyboard == null || key == null) return; _pointers.set_fake_pointer_state(key, kv, latched, lock); } /** Called by auto-capitalisation. */ public void set_shift_state(boolean latched, boolean lock) { set_fake_ptr_latched(_shift_key, KeyValue.SHIFT, latched, lock); } /** Called from [KeyEventHandler]. */ public void set_compose_pending(boolean pending) { set_fake_ptr_latched(_compose_key, KeyValue.COMPOSE, pending, false); } /** Called from [Keybard2.onUpdateSelection]. */ public void set_selection_state(boolean selection_state) { if (_config.editor_config.selection_mode_enabled) set_fake_ptr_latched(KeyboardData.Key.EMPTY, KeyValue.SELECTION_MODE, selection_state, true); } public KeyValue modifyKey(KeyValue k, Pointers.Modifiers mods) { return KeyModifier.modify(k, mods); } public void onPointerDown(KeyValue k, boolean isSwipe) { updateFlags(); _config.handler.key_down(k, isSwipe); invalidate(); vibrate(); } public void onPointerUp(KeyValue k, Pointers.Modifiers mods) { // [key_up] must be called before [updateFlags]. The latter might disable // flags. _config.handler.key_up(k, mods); updateFlags(); invalidate(); } public void onPointerHold(KeyValue k, Pointers.Modifiers mods) { _config.handler.key_up(k, mods); updateFlags(); } public void onPointerFlagsChanged(boolean shouldVibrate) { updateFlags(); invalidate(); if (shouldVibrate) vibrate(); } private void updateFlags() { _mods = _pointers.getModifiers(); _config.handler.mods_changed(_mods); } @Override public boolean onTouch(View v, MotionEvent event) { int p; switch (event.getActionMasked()) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: cancelGaugePress(); _pointers.onTouchUp(event.getPointerId(event.getActionIndex())); break; case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: p = event.getActionIndex(); float tx = event.getX(p); float ty = event.getY(p); KeyboardData.Key key = getKeyAtPosition(tx, ty); if (key != null && UsageGauge.isGaugeKey(key)) startGaugePress(); else if (key != null) _pointers.onTouchDown(tx, ty, event.getPointerId(p), key); break; case MotionEvent.ACTION_MOVE: if (_gaugePressed) { KeyboardData.Key moved = getKeyAtPosition(event.getX(0), event.getY(0)); if (moved == null || !UsageGauge.isGaugeKey(moved)) cancelGaugePress(); // slid off the gauge: no longer a press on it } for (p = 0; p < event.getPointerCount(); p++) _pointers.onTouchMove(event.getX(p), event.getY(p), event.getPointerId(p)); break; case MotionEvent.ACTION_CANCEL: cancelGaugePress(); _pointers.onTouchCancel(); break; default: return (false); } return (true); } private KeyboardData.Row getRowAtPosition(float ty) { float y = _config.marginTop; if (ty < y) return null; for (KeyboardData.Row row : _keyboard.rows) { y += (row.shift + row.height) * _tc.row_height; if (ty < y) return row; } return null; } private KeyboardData.Key getKeyAtPosition(float tx, float ty) { KeyboardData.Row row = getRowAtPosition(ty); float x = _marginLeft; if (row == null || tx < x) return null; for (KeyboardData.Key key : row.keys) { float xLeft = x + key.shift * _keyWidth; float xRight = xLeft + key.width * _keyWidth; if (tx < xLeft) return null; if (tx < xRight) return key; x = xRight; } return null; } private void vibrate() { VibratorCompat.vibrate(this, _config); } @Override public void onMeasure(int wSpec, int hSpec) { DisplayMetrics dm = getContext().getResources().getDisplayMetrics(); int width = dm.widthPixels; _marginLeft = Math.max(_config.horizontal_margin, _insets_left); _marginRight = Math.max(_config.horizontal_margin, _insets_right); _marginBottom = _config.margin_bottom + _insets_bottom; _keyWidth = (width - _marginLeft - _marginRight) / _keyboard.keysWidth; _tc = new Theme.Computed(_theme, _config, _keyWidth, _keyboard); // Compute the size of labels based on the width or the height of keys. The // margin around keys is taken into account. Keys normal aspect ratio is // assumed to be 3/2 for a 10 columns layout. It's generally more, the // width computation is useful when the keyboard is unusually high. float labelBaseSize = Math.min( _tc.row_height - _tc.vertical_margin, (width / 10 - _tc.horizontal_margin) * 3/2 ) * _config.characterSize; _mainLabelSize = labelBaseSize * _config.labelTextSize; _subLabelSize = labelBaseSize * _config.sublabelTextSize; int height = (int)(_tc.row_height * _keyboard.keysHeight + _config.marginTop + _marginBottom); setMeasuredDimension(width, height); } Rect _cached_exclusion_rect = new Rect(); List _cached_exclusion_rects = Arrays.asList(_cached_exclusion_rect); @Override public void onLayout(boolean changed, int left, int top, int right, int bottom) { if (!changed) return; // Since SDK 30, this is done automatically: // https://android.googlesource.com/platform/frameworks/base/+/android11-release/core/java/android/inputmethodservice/InputMethodService.java#852 if (VERSION.SDK_INT == 29) { // Disable the back-gesture on the keyboard area _cached_exclusion_rect.set( left + (int)_marginLeft, top + (int)_config.marginTop, right - (int)_marginRight, bottom - (int)_marginBottom); setSystemGestureExclusionRects(_cached_exclusion_rects); } } @Override public WindowInsets onApplyWindowInsets(WindowInsets wi) { // LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS is set in [Keyboard2#updateSoftInputWindowLayoutParams] for SDK_INT >= 35. if (VERSION.SDK_INT < 35) return wi; int insets_types = WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout(); Insets insets = wi.getInsets(insets_types); _insets_left = insets.left; _insets_right = insets.right; _insets_bottom = insets.bottom; return WindowInsets.CONSUMED; } /** Horizontal and vertical position of the 9 indexes. */ static final Paint.Align[] LABEL_POSITION_H = new Paint.Align[]{ Paint.Align.CENTER, Paint.Align.LEFT, Paint.Align.RIGHT, Paint.Align.LEFT, Paint.Align.RIGHT, Paint.Align.LEFT, Paint.Align.RIGHT, Paint.Align.CENTER, Paint.Align.CENTER }; static final Vertical[] LABEL_POSITION_V = new Vertical[]{ Vertical.CENTER, Vertical.TOP, Vertical.TOP, Vertical.BOTTOM, Vertical.BOTTOM, Vertical.CENTER, Vertical.CENTER, Vertical.TOP, Vertical.BOTTOM }; @Override protected void onDraw(Canvas canvas) { float y = _tc.margin_top; for (KeyboardData.Row row : _keyboard.rows) { y += row.shift * _tc.row_height; float x = _marginLeft + _tc.margin_left; float keyH = row.height * _tc.row_height - _tc.vertical_margin; for (KeyboardData.Key k : row.keys) { x += k.shift * _keyWidth; float keyW = _keyWidth * k.width - _tc.horizontal_margin; boolean isKeyDown = _pointers.isKeyDown(k); Theme.Computed.Key tc_key; if (isKeyDown) tc_key = _tc.key_activated; else switch (k.role) { case Action: tc_key = _tc.key_action; break; case Space_bar: tc_key = _tc.key_space_bar; break; case Suggestion: tc_key = _tc.key_suggestion; break; default: case Normal: tc_key = _tc.key; break; } drawKeyFrame(canvas, x, y, keyW, keyH, tc_key); if (UsageGauge.isGaugeKey(k)) drawUsageGauge(canvas, x, y, keyW, keyH, tc_key); else if (k.keys[0] != null) drawLabel(canvas, k.keys[0], keyW / 2f + x, y, keyH, isKeyDown, tc_key); for (int i = 1; i < 9; i++) { if (k.keys[i] != null) drawSubLabel(canvas, k.keys[i], x, y, keyW, keyH, i, isKeyDown, tc_key); } if (!UsageGauge.isGaugeKey(k)) drawIndication(canvas, k, x, y, keyW, keyH, _tc); x += _keyWidth * k.width; } y += row.height * _tc.row_height; } } /** Holding the gauge key opens the usage page. Short taps do nothing: the key never types. */ private void startGaugePress() { cancelGaugePress(); _gaugePressed = true; _gaugeLongPress = new Runnable() { public void run() { if (!_gaugePressed) return; _gaugePressed = false; _gaugeLongPress = null; openUsagePage(); } }; // Same delay the keyboard uses for every other long press, so it feels like the rest of it. _gaugeHandler.postDelayed(_gaugeLongPress, _config != null ? _config.longPressTimeout : 600L); } private void cancelGaugePress() { _gaugePressed = false; if (_gaugeLongPress != null) { _gaugeHandler.removeCallbacks(_gaugeLongPress); _gaugeLongPress = null; } } private void openUsagePage() { try { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(UsageGauge.USAGE_PAGE_URL)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getContext().startActivity(intent); } catch (Exception e) { // No browser, or the launcher refused: the keyboard must never crash over a long press. } } private void drawUsageGauge(Canvas canvas, float x, float y, float keyW, float keyH, Theme.Computed.Key tc) { UsageGauge.Reading reading = UsageGauge.cachedReading(); boolean stale = !_config.usageGaugeEnabled || UsageGauge.isStaleNow(_config.usageGaugeToken); float inset = Math.max(1.f, tc.border_width); RectF bounds = new RectF(x + inset, y + inset, x + keyW - inset, y + keyH - inset); GAUGE_PATH.reset(); GAUGE_PATH.addRoundRect(bounds, tc.border_radius, tc.border_radius, Path.Direction.CW); canvas.save(); canvas.clipPath(GAUGE_PATH); if (stale || reading == null) { GAUGE_PAINT.setShader(null); GAUGE_PAINT.setColor(0xff55585e); canvas.drawRect(bounds, GAUGE_PAINT); } else { float level = UsageGauge.fillFraction(reading); int hue = UsageGauge.hueColor(level); GAUGE_PAINT.setColor(tc.bg_paint.getColor()); canvas.drawRect(bounds, GAUGE_PAINT); if (level > 0) { float fillTop = bounds.bottom - bounds.height() * level; int darker = blendColor(hue, 0xff000000, 0.22f); GAUGE_PAINT.setShader(new LinearGradient(0, bounds.bottom, 0, fillTop, darker, hue, Shader.TileMode.CLAMP)); canvas.drawRect(bounds.left, fillTop, bounds.right, bounds.bottom, GAUGE_PAINT); GAUGE_PAINT.setShader(null); } } String label = reading == null ? "—" : String.format(Locale.US, "%.1f", reading.spentTodayUsd); float size = Math.min(_subLabelSize, keyW * 0.40f); GAUGE_PAINT.setTextSize(size); GAUGE_PAINT.setTypeface(android.graphics.Typeface.DEFAULT); GAUGE_PAINT.setTextAlign(Paint.Align.CENTER); // Stale hides the number behind a dash; a partial day keeps the number but dims it, so a // floor can never be read as the day's total. A low balance stays red: that reading is live. int labelColor = stale ? 0xff92949a : (UsageGauge.isLowBalance(reading) ? 0xffe53935 : (reading.partial ? 0xff92949a : _theme.labelColor)); if (!stale) { int luminance = (android.graphics.Color.red(labelColor) * 299 + android.graphics.Color.green(labelColor) * 587 + android.graphics.Color.blue(labelColor) * 114) / 1000; GAUGE_PAINT.setStyle(Paint.Style.STROKE); GAUGE_PAINT.setStrokeWidth(Math.max(1.f, size * 0.12f)); GAUGE_PAINT.setColor(luminance > 140 ? 0xff202020 : 0xfff5f5f5); canvas.drawText(label, bounds.centerX(), (keyH - GAUGE_PAINT.ascent() - GAUGE_PAINT.descent()) / 2f + y, GAUGE_PAINT); GAUGE_PAINT.setStyle(Paint.Style.FILL); } GAUGE_PAINT.setColor(labelColor); canvas.drawText(label, bounds.centerX(), (keyH - GAUGE_PAINT.ascent() - GAUGE_PAINT.descent()) / 2f + y, GAUGE_PAINT); canvas.restore(); } private static int blendColor(int from, int to, float amount) { return android.graphics.Color.rgb( (int)(android.graphics.Color.red(from) * (1 - amount) + android.graphics.Color.red(to) * amount), (int)(android.graphics.Color.green(from) * (1 - amount) + android.graphics.Color.green(to) * amount), (int)(android.graphics.Color.blue(from) * (1 - amount) + android.graphics.Color.blue(to) * amount)); } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); } /** Draw borders and background of the key. */ void drawKeyFrame(Canvas canvas, float x, float y, float keyW, float keyH, Theme.Computed.Key tc) { float r = tc.border_radius; float w = tc.border_width; float padding = w / 2.f; _tmpRect.set(x + padding, y + padding, x + keyW - padding, y + keyH - padding); canvas.drawRoundRect(_tmpRect, r, r, tc.bg_paint); if (w > 0.f) { float overlap = r - r * 0.85f + w; // sin(45°) drawBorder(canvas, x, y, x + overlap, y + keyH, tc.border_left_paint, tc); drawBorder(canvas, x + keyW - overlap, y, x + keyW, y + keyH, tc.border_right_paint, tc); drawBorder(canvas, x, y, x + keyW, y + overlap, tc.border_top_paint, tc); drawBorder(canvas, x, y + keyH - overlap, x + keyW, y + keyH, tc.border_bottom_paint, tc); } } /** Clip to draw a border at a time. This allows to call [drawRoundRect] several time with the same parameters but a different Paint. */ void drawBorder(Canvas canvas, float clipl, float clipt, float clipr, float clipb, Paint paint, Theme.Computed.Key tc) { float r = tc.border_radius; canvas.save(); canvas.clipRect(clipl, clipt, clipr, clipb); canvas.drawRoundRect(_tmpRect, r, r, paint); canvas.restore(); } private int labelColor(KeyValue k, boolean isKeyDown, boolean sublabel) { if (isKeyDown) { int flags = _pointers.getKeyFlags(k); if (flags != -1) { if ((flags & Pointers.FLAG_P_LOCKED) != 0) return _theme.lockedColor; return _theme.activatedColor; } return _theme.pressedColor; } if (k.hasFlagsAny(KeyValue.FLAG_SECONDARY | KeyValue.FLAG_GREYED)) { if (k.hasFlagsAny(KeyValue.FLAG_GREYED)) return _theme.greyedLabelColor; return _theme.secondaryLabelColor; } return sublabel ? _theme.subLabelColor : _theme.labelColor; } private void drawLabel(Canvas canvas, KeyValue kv, float x, float y, float keyH, boolean isKeyDown, Theme.Computed.Key tc) { kv = modifyKey(kv, _mods); if (kv == null) return; float textSize = scaleTextSize(kv, true); // DeepSeek peak/off-peak: the space bar's joystick glyph doubles as the rate // indicator (neon blue = off-peak, neon pink = peak). int color = kv.getKind() == KeyValue.Kind.Editing && KeyValue.Editing.SPACE_BAR.equals(kv.getEditing()) ? DeepSeekPeak.colorFor(System.currentTimeMillis()) : labelColor(kv, isKeyDown, false); // The emoji badge (0xE01D, Alt's top-right corner) sits in a corner slot, // which labelColor() would dim to secondaryLabelColor. Draw it in labelColor // so it matches the rest of the keyboard's labels. String glyph = kv.getString(); if (glyph != null && glyph.length() == 1 && glyph.charAt(0) == 0xE01D) color = _theme.labelColor; Paint p = tc.label_paint(kv.hasFlagsAny(KeyValue.FLAG_KEY_FONT), color, textSize); canvas.drawText(kv.getString(), x, (keyH - p.ascent() - p.descent()) / 2f + y, p); } private void drawSubLabel(Canvas canvas, KeyValue kv, float x, float y, float keyW, float keyH, int sub_index, boolean isKeyDown, Theme.Computed.Key tc) { Paint.Align a = LABEL_POSITION_H[sub_index]; Vertical v = LABEL_POSITION_V[sub_index]; kv = modifyKey(kv, _mods); if (kv == null) return; float textSize = scaleTextSize(kv, false); Paint p = tc.sublabel_paint(kv.hasFlagsAny(KeyValue.FLAG_KEY_FONT), labelColor(kv, isKeyDown, true), textSize, a); float subPadding = _config.keyPadding; if (v == Vertical.CENTER) y += (keyH - p.ascent() - p.descent()) / 2f; else y += (v == Vertical.TOP) ? subPadding - p.ascent() : keyH - subPadding - p.descent(); if (a == Paint.Align.CENTER) x += keyW / 2f; else x += (a == Paint.Align.LEFT) ? subPadding : keyW - subPadding; String label = kv.getString(); int label_len = label.length(); // Limit the label of string keys to 3 characters if (label_len > 3 && kv.getKind() == KeyValue.Kind.String) label_len = 3; canvas.drawText(label, 0, label_len, x, y, p); } private void drawIndication(Canvas canvas, KeyboardData.Key k, float x, float y, float keyW, float keyH, Theme.Computed tc) { if (k.indication == null || k.indication.equals("")) return; Paint p = tc.indication_paint; p.setTextSize(_subLabelSize); canvas.drawText(k.indication, 0, k.indication.length(), x + keyW / 2f, (keyH - p.ascent() - p.descent()) * 4/5 + y, p); } private float scaleTextSize(KeyValue k, boolean main_label) { float smaller_font = k.hasFlagsAny(KeyValue.FLAG_SMALLER_FONT) ? 0.75f : 1.f; float label_size = main_label ? _mainLabelSize : _subLabelSize; return label_size * smaller_font; } }