content: /*
   * Copyright (C) 2014 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 com.android.server.location;
  
  import android.annotation.NonNull;
  import android.app.AppOpsManager;
  import android.content.Context;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.IInterface;
  import android.os.RemoteException;
  import android.util.Log;
  
  import com.android.internal.util.Preconditions;
  
  import java.util.HashMap;
  import java.util.Map;
  
  /**
   * A helper class that handles operations in remote listeners.
   *
   * @param <TListener> the type of GNSS data listener.
   */
  public abstract class RemoteListenerHelper<TListener extends IInterface> {
  
      protected static final int RESULT_SUCCESS = 0;
      protected static final int RESULT_NOT_AVAILABLE = 1;
      protected static final int RESULT_NOT_SUPPORTED = 2;
      protected static final int RESULT_GPS_LOCATION_DISABLED = 3;
      protected static final int RESULT_INTERNAL_ERROR = 4;
      protected static final int RESULT_UNKNOWN = 5;
      protected static final int RESULT_NOT_ALLOWED = 6;
  
      protected final Handler mHandler;
      private final String mTag;
  
      private final Map<IBinder, IdentifiedListener> mListenerMap = new HashMap<>();
  
      protected final Context mContext;
      protected final AppOpsManager mAppOps;
  
      private volatile boolean mIsRegistered;  // must access only on handler thread, or read-only
  
      private boolean mHasIsSupported;
      private boolean mIsSupported;
  
      private int mLastReportedResult = RESULT_UNKNOWN;
  
      protected RemoteListenerHelper(Context context, Handler handler, String name) {
          Preconditions.checkNotNull(name);
          mHandler = handler;
          mTag = name;
          mContext = context;
          mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
      }
  
      // read-only access for a dump() thread assured via volatile
      public boolean isRegistered() {
          return mIsRegistered;
      }
  
      /**
       * Adds GNSS data listener {@code listener} with caller identify {@code callerIdentify}.
       */
      public void addListener(@NonNull TListener listener, CallerIdentity callerIdentity) {
          Preconditions.checkNotNull(listener, "Attempted to register a 'null' listener.");
          IBinder binder = listener.asBinder();
          synchronized (mListenerMap) {
              if (mListenerMap.containsKey(binder)) {
                  // listener already added
                  return;
              }
  
              IdentifiedListener identifiedListener = new IdentifiedListener(listener,
                      callerIdentity);
              mListenerMap.put(binder, identifiedListener);
  
              // update statuses we already know about, starting from the ones that will never change
              int result;
              if (!isAvailableInPlatform()) {
                  result = RESULT_NOT_AVAILABLE;
              } else if (mHasIsSupported && !mIsSupported) {
                  result = RESULT_NOT_SUPPORTED;
              } else if (!isGpsEnabled()) {
                  // only attempt to register if GPS is enabled, otherwise we will register once GPS
                  // becomes available
                  result = RESULT_GPS_LOCATION_DISABLED;
              } else if (mHasIsSupported && mIsSupported) {
                  tryRegister();
                  // initially presume success, possible internal error could follow asynchornously
                  result = RESULT_SUCCESS;
              } else {
                  // at this point if the supported flag is not set, the notification will be sent
                  // asynchronously in the future
                  return;
              }
              post(identifiedListener, getHandlerOperation(result));
          }
      }
  
      /**
       * Remove GNSS data listener {@code listener}.
       */
      public void removeListener(@NonNull TListener listener) {
          Preconditions.checkNotNull(listener, "Attempted to remove a 'null' listener.");
          synchronized (mListenerMap) {
              mListenerMap.remove(listener.asBinder());
              if (mListenerMap.isEmpty()) {
                  tryUnregister();
              }
          }
      }
  
      protected abstract boolean isAvailableInPlatform();
      protected abstract boolean isGpsEnabled();
      // must access only on handler thread
      protected abstract int registerWithService();
      protected abstract void unregisterFromService(); // must access only on handler thread
      protected abstract ListenerOperation<TListener> getHandlerOperation(int result);
  
      protected interface ListenerOperation<TListener extends IInterface> {
          void execute(TListener listener, CallerIdentity callerIdentity) throws RemoteException;
      }
  
      protected void foreach(ListenerOperation<TListener> operation) {
          synchronized (mListenerMap) {
              foreachUnsafe(operation);
          }
      }
  
      protected void setSupported(boolean value) {
          synchronized (mListenerMap) {
              mHasIsSupported = true;
              mIsSupported = value;
          }
      }
  
      protected void tryUpdateRegistrationWithService() {
          synchronized (mListenerMap) {
              if (!isGpsEnabled()) {
                  tryUnregister();
                  return;
              }
              if (mListenerMap.isEmpty()) {
                  return;
              }
              tryRegister();
          }
      }
  
      protected void updateResult() {
          synchronized (mListenerMap) {
              int newResult = calculateCurrentResultUnsafe();
              if (mLastReportedResult == newResult) {
                  return;
              }
              foreachUnsafe(getHandlerOperation(newResult));
              mLastReportedResult = newResult;
          }
      }
  
      protected boolean hasPermission(Context context, CallerIdentity callerIdentity) {
          if (LocationPermissionUtil.doesCallerReportToAppOps(context, callerIdentity)) {
              // The caller is identified as a location provider that will report location
              // access to AppOps. Skip noteOp but do checkOp to check for location permission.
              return mAppOps.checkOpNoThrow(AppOpsManager.OP_FINE_LOCATION, callerIdentity.mUid,
                      callerIdentity.mPackageName) == AppOpsManager.MODE_ALLOWED;
          }
  
          return mAppOps.noteOpNoThrow(AppOpsManager.OP_FINE_LOCATION, callerIdentity.mUid,
                  callerIdentity.mPackageName) == AppOpsManager.MODE_ALLOWED;
      }
  
      protected void logPermissionDisabledEventNotReported(String tag, String packageName,
              String event) {
          if (Log.isLoggable(tag, Log.DEBUG)) {
              Log.d(tag, "Location permission disabled. Skipping " + event + " reporting for app: "
                      + packageName);
          }
      }
  
      private void foreachUnsafe(ListenerOperation<TListener> operation) {
          for (IdentifiedListener identifiedListener : mListenerMap.values()) {
              post(identifiedListener, operation);
          }
      }
  
      private void post(IdentifiedListener identifiedListener,
              ListenerOperation<TListener> operation) {
          if (operation != null) {
              mHandler.post(new HandlerRunnable(identifiedListener, operation));
          }
      }
  
      private void tryRegister() {
          mHandler.post(new Runnable() {
              int registrationState = RESULT_INTERNAL_ERROR;
              @Override
              public void run() {
                  if (!mIsRegistered) {
                      registrationState = registerWithService();
                      mIsRegistered = registrationState == RESULT_SUCCESS;
                  }
                  if (!mIsRegistered) {
                      // post back a failure
                      mHandler.post(() -> {
                          synchronized (mListenerMap) {
                              foreachUnsafe(getHandlerOperation(registrationState));
                          }
                      });
                  }
              }
          });
      }
  
      private void tryUnregister() {
          mHandler.post(() -> {
                      if (!mIsRegistered) {
                          return;
                      }
                      unregisterFromService();
                      mIsRegistered = false;
                  }
          );
      }
  
      private int calculateCurrentResultUnsafe() {
          // update statuses we already know about, starting from the ones that will never change
          if (!isAvailableInPlatform()) {
              return RESULT_NOT_AVAILABLE;
          }
          if (!mHasIsSupported || mListenerMap.isEmpty()) {
              // we'll update once we have a supported status available
              return RESULT_UNKNOWN;
          }
          if (!mIsSupported) {
              return RESULT_NOT_SUPPORTED;
          }
          if (!isGpsEnabled()) {
              return RESULT_GPS_LOCATION_DISABLED;
          }
          return RESULT_SUCCESS;
      }
  
      private class IdentifiedListener {
          private final TListener mListener;
          private final CallerIdentity mCallerIdentity;
  
          private IdentifiedListener(@NonNull TListener listener, CallerIdentity callerIdentity) {
              mListener = listener;
              mCallerIdentity = callerIdentity;
          }
      }
  
      private class HandlerRunnable implements Runnable {
          private final IdentifiedListener mIdentifiedListener;
          private final ListenerOperation<TListener> mOperation;
  
          private HandlerRunnable(IdentifiedListener identifiedListener,
                  ListenerOperation<TListener> operation) {
              mIdentifiedListener = identifiedListener;
              mOperation = operation;
          }
  
          @Override
          public void run() {
              try {
                  mOperation.execute(mIdentifiedListener.mListener,
                          mIdentifiedListener.mCallerIdentity);
              } catch (RemoteException e) {
                  Log.v(mTag, "Error in monitored listener.", e);
              }
          }
      }
  }
  