content: /*
   * Copyright (C) 2017 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.accessibilityservice;
  
  import android.annotation.NonNull;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.RemoteException;
  import android.util.ArrayMap;
  import android.util.Slog;
  
  import com.android.internal.util.Preconditions;
  
  /**
   * Controller for the accessibility button within the system's navigation area
   * <p>
   * This class may be used to query the accessibility button's state and register
   * callbacks for interactions with and state changes to the accessibility button when
   * {@link AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON} is set.
   * </p>
   * <p>
   * <strong>Note:</strong> This class and
   * {@link AccessibilityServiceInfo#FLAG_REQUEST_ACCESSIBILITY_BUTTON} should not be used as
   * the sole means for offering functionality to users via an {@link AccessibilityService}.
   * Some device implementations may choose not to provide a software-rendered system
   * navigation area, making this affordance permanently unavailable.
   * </p>
   * <p>
   * <strong>Note:</strong> On device implementations where the accessibility button is
   * supported, it may not be available at all times, such as when a foreground application uses
   * {@link android.view.View#SYSTEM_UI_FLAG_HIDE_NAVIGATION}. A user may also choose to assign
   * this button to another accessibility service or feature. In each of these cases, a
   * registered {@link AccessibilityButtonCallback}'s
   * {@link AccessibilityButtonCallback#onAvailabilityChanged(AccessibilityButtonController, boolean)}
   * method will be invoked to provide notifications of changes in the accessibility button's
   * availability to the registering service.
   * </p>
   */
  public final class AccessibilityButtonController {
      private static final String LOG_TAG = "A11yButtonController";
  
      private final IAccessibilityServiceConnection mServiceConnection;
      private final Object mLock;
      private ArrayMap<AccessibilityButtonCallback, Handler> mCallbacks;
  
      AccessibilityButtonController(@NonNull IAccessibilityServiceConnection serviceConnection) {
          mServiceConnection = serviceConnection;
          mLock = new Object();
      }
  
      /**
       * Retrieves whether the accessibility button in the system's navigation area is
       * available to the calling service.
       * <p>
       * <strong>Note:</strong> If the service is not yet connected (e.g.
       * {@link AccessibilityService#onServiceConnected()} has not yet been called) or the
       * service has been disconnected, this method will have no effect and return {@code false}.
       * </p>
       *
       * @return {@code true} if the accessibility button in the system's navigation area is
       * available to the calling service, {@code false} otherwise
       */
      public boolean isAccessibilityButtonAvailable() {
          if (mServiceConnection != null) {
              try {
                  return mServiceConnection.isAccessibilityButtonAvailable();
              } catch (RemoteException re) {
                  Slog.w(LOG_TAG, "Failed to get accessibility button availability.", re);
                  re.rethrowFromSystemServer();
                  return false;
              }
          }
          return false;
      }
  
      /**
       * Registers the provided {@link AccessibilityButtonCallback} for interaction and state
       * changes callbacks related to the accessibility button.
       *
       * @param callback the callback to add, must be non-null
       */
      public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback) {
          registerAccessibilityButtonCallback(callback, new Handler(Looper.getMainLooper()));
      }
  
      /**
       * Registers the provided {@link AccessibilityButtonCallback} for interaction and state
       * change callbacks related to the accessibility button. The callback will occur on the
       * specified {@link Handler}'s thread, or on the services's main thread if the handler is
       * {@code null}.
       *
       * @param callback the callback to add, must be non-null
       * @param handler the handler on which the callback should execute, must be non-null
       */
      public void registerAccessibilityButtonCallback(@NonNull AccessibilityButtonCallback callback,
              @NonNull Handler handler) {
          Preconditions.checkNotNull(callback);
          Preconditions.checkNotNull(handler);
          synchronized (mLock) {
              if (mCallbacks == null) {
                  mCallbacks = new ArrayMap<>();
              }
  
              mCallbacks.put(callback, handler);
          }
      }
  
      /**
       * Unregisters the provided {@link AccessibilityButtonCallback} for interaction and state
       * change callbacks related to the accessibility button.
       *
       * @param callback the callback to remove, must be non-null
       */
      public void unregisterAccessibilityButtonCallback(
              @NonNull AccessibilityButtonCallback callback) {
          Preconditions.checkNotNull(callback);
          synchronized (mLock) {
              if (mCallbacks == null) {
                  return;
              }
  
              final int keyIndex = mCallbacks.indexOfKey(callback);
              final boolean hasKey = keyIndex >= 0;
              if (hasKey) {
                  mCallbacks.removeAt(keyIndex);
              }
          }
      }
  
      /**
       * Dispatches the accessibility button click to any registered callbacks. This should
       * be called on the service's main thread.
       */
      void dispatchAccessibilityButtonClicked() {
          final ArrayMap<AccessibilityButtonCallback, Handler> entries;
          synchronized (mLock) {
              if (mCallbacks == null || mCallbacks.isEmpty()) {
                  Slog.w(LOG_TAG, "Received accessibility button click with no callbacks!");
                  return;
              }
  
              // Callbacks may remove themselves. Perform a shallow copy to avoid concurrent
              // modification.
              entries = new ArrayMap<>(mCallbacks);
          }
  
          for (int i = 0, count = entries.size(); i < count; i++) {
              final AccessibilityButtonCallback callback = entries.keyAt(i);
              final Handler handler = entries.valueAt(i);
              handler.post(() -> callback.onClicked(this));
          }
      }
  
      /**
       * Dispatches the accessibility button availability changes to any registered callbacks.
       * This should be called on the service's main thread.
       */
      void dispatchAccessibilityButtonAvailabilityChanged(boolean available) {
          final ArrayMap<AccessibilityButtonCallback, Handler> entries;
          synchronized (mLock) {
              if (mCallbacks == null || mCallbacks.isEmpty()) {
                  Slog.w(LOG_TAG,
                          "Received accessibility button availability change with no callbacks!");
                  return;
              }
  
              // Callbacks may remove themselves. Perform a shallow copy to avoid concurrent
              // modification.
              entries = new ArrayMap<>(mCallbacks);
          }
  
          for (int i = 0, count = entries.size(); i < count; i++) {
              final AccessibilityButtonCallback callback = entries.keyAt(i);
              final Handler handler = entries.valueAt(i);
              handler.post(() -> callback.onAvailabilityChanged(this, available));
          }
      }
  
      /**
       * Callback for interaction with and changes to state of the accessibility button
       * within the system's navigation area.
       */
      public static abstract class AccessibilityButtonCallback {
  
          /**
           * Called when the accessibility button in the system's navigation area is clicked.
           *
           * @param controller the controller used to register for this callback
           */
          public void onClicked(AccessibilityButtonController controller) {}
  
          /**
           * Called when the availability of the accessibility button in the system's
           * navigation area has changed. The accessibility button may become unavailable
           * because the device shopped showing the button, the button was assigned to another
           * service, or for other reasons.
           *
           * @param controller the controller used to register for this callback
           * @param available {@code true} if the accessibility button is available to this
           *                  service, {@code false} otherwise
           */
          public void onAvailabilityChanged(AccessibilityButtonController controller,
                  boolean available) {
          }
      }
  }
  