12d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak/*
22d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * Copyright (C) 2017 The Android Open Source Project
32d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak *
42d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * Licensed under the Apache License, Version 2.0 (the "License");
52d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * you may not use this file except in compliance with the License.
62d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * You may obtain a copy of the License at
72d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak *
82d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak *      http://www.apache.org/licenses/LICENSE-2.0
92d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak *
102d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * Unless required by applicable law or agreed to in writing, software
112d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * distributed under the License is distributed on an "AS IS" BASIS,
122d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
132d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * See the License for the specific language governing permissions and
142d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * limitations under the License.
152d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak */
162d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzakpackage com.android.managedprovisioning.common;
172d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
182d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzakimport android.graphics.Rect;
192d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzakimport android.util.DisplayMetrics;
202d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzakimport android.view.TouchDelegate;
212d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzakimport android.view.View;
222d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
232d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzakimport com.android.internal.annotations.VisibleForTesting;
242d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
252d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak/**
262d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * Allows for expanding touch area of a {@link View} element, so it's compliant with
272d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * accessibility guidelines, while not modifying the UI appearance.
282d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak * @see <a href="https://goo.gl/FcU5gX">Android Accessibility Guide</a>
292d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak */
302d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzakpublic class TouchTargetEnforcer {
312d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    /** Value taken from Android Accessibility Guide */
322d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    @VisibleForTesting static final int MIN_TARGET_DP = 48;
332d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
342d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    /** @see DisplayMetrics#density */
352d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    private final float mDensity;
362d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
372d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    private final TouchDelegateProvider mTouchDelegateProvider;
382d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
392d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    /**
402d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     * Allows for expanding touch area of a {@link View} element, so it's compliant with
412d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     * accessibility guidelines, while not modifying the UI appearance.
422d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     * @param density {@link DisplayMetrics#density}
432d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     * @see <a href="https://goo.gl/FcU5gX">Android Accessibility Guide</a>
442d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     */
452d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    public TouchTargetEnforcer(float density) {
462d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        this(density, TouchDelegate::new);
472d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    }
482d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
492d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    /**
502d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     * Allows for expanding touch area of a {@link View} element, so it's compliant with
512d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     * accessibility guidelines, while not modifying the UI appearance.
522d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     * @param density {@link DisplayMetrics#density}
532d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     * @see <a href="https://goo.gl/FcU5gX">Android Accessibility Guide</a>
542d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     */
552d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    TouchTargetEnforcer(float density, TouchDelegateProvider touchDelegateProvider) {
562d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        mDensity = density;
572d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        mTouchDelegateProvider = touchDelegateProvider;
582d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    }
592d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
602d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    /**
612d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     * Compares target's touch area to required minimum, and expands it if necessary.
622d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     * <p>FIXME: Does not honor screen boundaries, so might set touch areas outside of the screen.
632d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     * <p>FIXME: Does not honor ancestor boundaries, so might not work if ancestor too small.
642d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     * <p>FIXME: Does not work if ancestor has more than one TouchTarget set.
652d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     * @param target element to check for accessibility compliance
662d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     * @param ancestor target's ancestor - only one target per ancestor allowed
672d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak     */
682d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    public void enforce(View target, View ancestor) {
692d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        target.getViewTreeObserver().addOnGlobalLayoutListener( // avoids some subtle bugs
702d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                () -> {
712d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                    int minTargetPx = (int) Math.ceil(dpToPx(MIN_TARGET_DP));
722d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                    int deltaHeight = Math.max(0, minTargetPx - target.getHeight());
732d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                    int deltaWidth = Math.max(0, minTargetPx - target.getWidth());
742d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                    if (deltaHeight <= 0 && deltaWidth <= 0) {
752d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                        return;
762d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                    }
772d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
782d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                    ancestor.post(() -> {
792d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                        Rect bounds = createNewBounds(target, minTargetPx, deltaWidth, deltaHeight);
802d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
812d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                        synchronized (ancestor) {
822d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                            if (ancestor.getTouchDelegate() == null) {
832d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                                ancestor.setTouchDelegate(
842d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                                        mTouchDelegateProvider.getInstance(bounds, target));
852d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                                ProvisionLogger.logd(String.format(
862d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                                        "Successfully set touch delegate on ancestor %s "
872d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                                                + "delegating to target %s.",
882d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                                        ancestor, target));
892d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                            } else {
902d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                                ProvisionLogger.logd(String.format(
912d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                                        "Ancestor %s already has an assigned touch delegate %s. "
922d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                                                + "Unable to assign another one. Ignoring target.",
932d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                                        ancestor, target));
942d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                            }
952d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                        }
962d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                    });
972d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak                });
982d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    }
992d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
1002d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    private Rect createNewBounds(View target, int minTargetPx, int deltaWidth, int deltaHeight) {
1012d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        int deltaWidthHalf = deltaWidth / 2;
1022d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        int deltaHeightHalf = deltaHeight / 2;
1032d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
1042d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        Rect result = new Rect();
1052d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        target.getHitRect(result);
1062d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        result.top -= deltaHeightHalf;
1072d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        result.bottom += deltaHeightHalf;
1082d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        result.left -= deltaWidthHalf;
1092d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        result.right += deltaWidthHalf;
1102d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
1112d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        // fix rounding errors
1122d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        int deltaHeightRemaining = minTargetPx - (result.bottom - result.top);
1132d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        if (deltaHeightRemaining > 0) {
1142d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak            result.bottom += deltaHeightRemaining;
1152d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        }
1162d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        int deltaWidthRemaining = minTargetPx - (result.right - result.left);
1172d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        if (deltaWidthRemaining > 0) {
1182d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak            result.right += deltaWidthRemaining;
1192d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        }
1202d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        return result;
1212d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    }
1222d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
1232d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    private float dpToPx(int dp) {
1242d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        return dp * mDensity;
1252d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    }
1262d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak
1272d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    interface TouchDelegateProvider {
1282d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        /**
1292d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak         * @param bounds New touch bounds
1302d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak         * @param delegateView The view that should receive motion events (target)
1312d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak         */
1322d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak        TouchDelegate getInstance(Rect bounds, View delegateView);
1332d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak    }
1342d9ad3273f375a84cf32ea33b55a9bcde43d7bc7Jakub Gielzak}
135