void generateServicesMap() {
   Iterator i$;
   PackageManager pm = this.mContext.getPackageManager();
   ArrayList<ServiceInfo<AuthenticatorDescription>> serviceInfos = new ArrayList();
   for (ResolveInfo resolveInfo : pm.queryIntentServices(new Intent(this.mInterfaceName), 128)) {
     ServiceInfo<AuthenticatorDescription> info;
     try {
       info = parseServiceInfo(resolveInfo);
       if (info == null) {
         Log.w(TAG, "Unable to load service info " + resolveInfo.toString());
       } else {
         serviceInfos.add(info);
       }
     } catch (XmlPullParserException e) {
       Log.w(TAG, "Unable to load service info " + resolveInfo.toString(), e);
     } catch (IOException e2) {
       Log.w(TAG, "Unable to load service info " + resolveInfo.toString(), e2);
     }
   }
   synchronized (this.mServicesLock) {
     this.mServices = Maps.newHashMap();
     i$ = serviceInfos.iterator();
     while (i$.hasNext()) {
       info = (ServiceInfo) i$.next();
       this.mServices.put(info.type, info);
     }
   }
 }
Example #2
0
/**
 * Creates a Call model from Call state and data received from the telephony layer. The telephony
 * layer maintains 3 conceptual objects: Phone, Call, Connection.
 *
 * <p>Phone represents the radio and there is an implementation per technology type such as
 * GSMPhone, SipPhone, CDMAPhone, etc. Generally, we will only ever deal with one instance of this
 * object for the lifetime of this class.
 *
 * <p>There are 3 Call instances that exist for the lifetime of this class which are created by
 * CallTracker. The three are RingingCall, ForegroundCall, and BackgroundCall.
 *
 * <p>A Connection most closely resembles what the layperson would consider a call. A Connection is
 * created when a user dials and it is "owned" by one of the three Call instances. Which of the
 * three Calls owns the Connection changes as the Connection goes between ACTIVE, HOLD, RINGING, and
 * other states.
 *
 * <p>This class models a new Call class from Connection objects received from the telephony layer.
 * We use Connection references as identifiers for a call; new reference = new call.
 *
 * <p>TODO: Create a new Call class to replace the simple call Id ints being used currently.
 *
 * <p>The new Call models are parcellable for transfer via the CallHandlerService API.
 */
public class CallModeler extends Handler {

  private static final String TAG = CallModeler.class.getSimpleName();
  private static final boolean DBG =
      (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);

  private static final int CALL_ID_START_VALUE = 1;

  private final CallStateMonitor mCallStateMonitor;
  private final CallManager mCallManager;
  private final CallGatewayManager mCallGatewayManager;
  private final HashMap<Connection, Call> mCallMap = Maps.newHashMap();
  private final HashMap<Connection, Call> mConfCallMap = Maps.newHashMap();
  private final AtomicInteger mNextCallId = new AtomicInteger(CALL_ID_START_VALUE);
  private final ArrayList<Listener> mListeners = new ArrayList<Listener>();
  private Connection mCdmaIncomingConnection;
  private Connection mCdmaOutgoingConnection;
  private boolean mNextGsmCallIsForwarded;
  private SuppServiceNotification mSuppSvcNotification;
  private boolean mVoicePrivacyState = false;

  public CallModeler(
      CallStateMonitor callStateMonitor,
      CallManager callManager,
      CallGatewayManager callGatewayManager) {
    mCallStateMonitor = callStateMonitor;
    mCallManager = callManager;
    mCallGatewayManager = callGatewayManager;

    mCallStateMonitor.addListener(this);
  }

  @Override
  public void handleMessage(Message msg) {
    switch (msg.what) {
      case CallStateMonitor.PHONE_NEW_RINGING_CONNECTION:
        // We let the CallNotifier handle the new ringing connection first. When the custom
        // ringtone and send_to_voicemail settings are retrieved, CallNotifier will directly
        // call CallModeler's onNewRingingConnection.
        break;
      case CallStateMonitor.PHONE_DISCONNECT:
        onDisconnect((Connection) ((AsyncResult) msg.obj).result);
        break;
      case CallStateMonitor.PHONE_UNKNOWN_CONNECTION_APPEARED:
        // fall through
      case CallStateMonitor.PHONE_STATE_CHANGED:
        onPhoneStateChanged((AsyncResult) msg.obj);
        break;
      case CallStateMonitor.PHONE_ON_DIAL_CHARS:
        onPostDialChars((AsyncResult) msg.obj, (char) msg.arg1);
        break;
      case CallStateMonitor.PHONE_SUPP_SERVICE_NOTIFY:
        if (DBG) Log.d(TAG, "Received Supplementary Notification");
        if (msg.obj != null) {
          onSuppServiceNotification((AsyncResult) msg.obj);
        }
        break;
      case CallStateMonitor.PHONE_ACTIVE_SUBSCRIPTION_CHANGE:
        onActiveSubChanged((AsyncResult) msg.obj);
        break;
      case CallStateMonitor.PHONE_SUPP_SERVICE_FAILED:
        AsyncResult r = (AsyncResult) msg.obj;
        Phone.SuppService service = (Phone.SuppService) r.result;
        int val = service.ordinal();
        if (DBG) Log.d(TAG, "SUPP_SERVICE_FAILED..." + service);
        for (int i = 0; i < mListeners.size(); i++) {
          mListeners.get(i).onSuppServiceFailed(val);
        }
        break;
      case CallStateMonitor.PHONE_ENHANCED_VP_ON:
        if (DBG) Log.d(TAG, "PHONE_ENHANCED_VP_ON...");
        if (!mVoicePrivacyState) {
          mVoicePrivacyState = true;
          onPhoneStateChanged(null);
        }
        break;
      case CallStateMonitor.PHONE_ENHANCED_VP_OFF:
        if (DBG) Log.d(TAG, "PHONE_ENHANCED_VP_OFF...");
        if (mVoicePrivacyState) {
          mVoicePrivacyState = false;
          onPhoneStateChanged(null);
        }
        break;

      default:
        break;
    }
  }

  public void addListener(Listener listener) {
    Preconditions.checkNotNull(listener);
    Preconditions.checkNotNull(mListeners);
    if (!mListeners.contains(listener)) {
      mListeners.add(listener);
    }
  }

  public List<Call> getFullList() {
    final List<Call> calls = Lists.newArrayListWithCapacity(mCallMap.size() + mConfCallMap.size());
    calls.addAll(mCallMap.values());
    calls.addAll(mConfCallMap.values());
    return calls;
  }

  public CallResult getCallWithId(int callId) {
    // max 8 connections, so this should be fast even through we are traversing the entire map.
    for (Entry<Connection, Call> entry : mCallMap.entrySet()) {
      if (entry.getValue().getCallId() == callId) {
        return new CallResult(entry.getValue(), entry.getKey());
      }
    }

    for (Entry<Connection, Call> entry : mConfCallMap.entrySet()) {
      if (entry.getValue().getCallId() == callId) {
        return new CallResult(entry.getValue(), entry.getKey());
      }
    }
    return null;
  }

  public boolean hasLiveCall() {
    return hasLiveCallInternal(mCallMap) || hasLiveCallInternal(mConfCallMap);
  }

  public void onCdmaCallWaiting(CdmaCallWaitingNotification callWaitingInfo) {
    // We dont get the traditional onIncomingCall notification for cdma call waiting,
    // but the Connection does actually exist.  We need to find it in the set of ringing calls
    // and pass it through our normal incoming logic.
    final com.android.internal.telephony.Call teleCall = mCallManager.getFirstActiveRingingCall();

    if (teleCall.getState() == com.android.internal.telephony.Call.State.WAITING) {
      Connection connection = teleCall.getLatestConnection();

      if (connection != null) {
        String number = connection.getAddress();
        if (number != null && number.equals(callWaitingInfo.number)) {
          Call call = onNewRingingConnection(connection);
          mCdmaIncomingConnection = connection;
          return;
        }
      }
    }

    Log.e(TAG, "CDMA Call waiting notification without a matching connection.");
  }

  public void onCdmaCallWaitingReject() {
    // Cdma call was rejected...
    if (mCdmaIncomingConnection != null) {
      onDisconnect(mCdmaIncomingConnection);
      mCdmaIncomingConnection = null;
    } else {
      Log.e(TAG, "CDMA Call waiting rejection without an incoming call.");
    }
  }

  /**
   * CDMA Calls have no sense of "dialing" state. For outgoing calls 3way calls we want to mimick
   * this state so that the the UI can notify the user that there is a "dialing" call.
   */
  public void setCdmaOutgoing3WayCall(Connection connection) {
    boolean wasSet = mCdmaOutgoingConnection != null;

    mCdmaOutgoingConnection = connection;

    // If we reset the connection, that mean we can now tell the user that the call is actually
    // part of the conference call and move it out of the dialing state. To do this, issue a
    // new update completely.
    if (wasSet && mCdmaOutgoingConnection == null) {
      onPhoneStateChanged(null);
    }
  }

  private boolean hasLiveCallInternal(HashMap<Connection, Call> map) {
    for (Call call : map.values()) {
      final int state = call.getState();
      if (state == Call.State.ACTIVE
          || state == Call.State.CALL_WAITING
          || state == Call.State.CONFERENCED
          || state == Call.State.DIALING
          || state == Call.State.REDIALING
          || state == Call.State.INCOMING
          || state == Call.State.ONHOLD
          || state == Call.State.DISCONNECTING) {
        return true;
      }
    }
    return false;
  }

  public boolean hasOutstandingActiveOrDialingCall() {
    return hasOutstandingActiveOrDialingCallInternal(mCallMap)
        || hasOutstandingActiveOrDialingCallInternal(mConfCallMap);
  }

  private static boolean hasOutstandingActiveOrDialingCallInternal(HashMap<Connection, Call> map) {
    for (Call call : map.values()) {
      final int state = call.getState();
      if (state == Call.State.ACTIVE || Call.State.isDialing(state)) {
        return true;
      }
    }

    return false;
  }

  /**
   * Handles the POST_ON_DIAL_CHARS message from the Phone (see our call to
   * mPhone.setOnPostDialCharacter() above.)
   *
   * <p>TODO: NEED TO TEST THIS SEQUENCE now that we no longer handle "dialable" key events here in
   * the InCallScreen: we do directly to the Dialer UI instead. Similarly, we may now need to go
   * directly to the Dialer to handle POST_ON_DIAL_CHARS too.
   */
  private void onPostDialChars(AsyncResult r, char ch) {
    final Connection c = (Connection) r.result;

    if (c != null) {
      final Connection.PostDialState state = (Connection.PostDialState) r.userObj;

      switch (state) {
        case WAIT:
          final Call call = getCallFromMap(mCallMap, c, false);
          if (call == null) {
            Log.i(TAG, "Call no longer exists. Skipping onPostDialWait().");
          } else {
            for (Listener mListener : mListeners) {
              mListener.onPostDialAction(
                  state, call.getCallId(), c.getRemainingPostDialString(), ch);
            }
          }
          break;
        default:
          // This is primarily to cause the DTMFTonePlayer to play local tones.
          // Other listeners simply perform no-ops.
          for (Listener mListener : mListeners) {
            mListener.onPostDialAction(state, 0, "", ch);
          }
          break;
      }
    }
  }

  /* package */ Call onNewRingingConnection(Connection conn) {
    Log.i(TAG, "onNewRingingConnection");
    final Call call = getCallFromMap(mCallMap, conn, true);

    if (call != null) {
      Phone phone = conn.getCall().getPhone();
      if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM && mNextGsmCallIsForwarded) {
        call.setForwarded(true);
        mNextGsmCallIsForwarded = false;
      }

      updateCallFromConnection(call, conn, false);
      for (int i = 0; i < mListeners.size(); ++i) {
        mListeners.get(i).onIncoming(call);
      }
      if (MSimTelephonyManager.getDefault().isMultiSimEnabled()) {
        int subscription = conn.getCall().getPhone().getSubscription();
        Log.i(TAG, "Setting Active sub : '" + subscription + "'");
        PhoneUtils.setActiveSubscription(subscription);
        // if any local hold tones are playing then they need to be stoped.
        final MSimCallNotifier notifier = (MSimCallNotifier) PhoneGlobals.getInstance().notifier;
        notifier.manageLocalCallWaitingTone();
      }
    }

    PhoneGlobals.getInstance().updateWakeState();
    return call;
  }

  /* package */ void onUnsolCallModify(Connection conn) {
    final Call call = getCallFromMap(mCallMap, conn, false);
    copyDetails(
        conn.getCallModify().call_details,
        call.getCallModifyDetails(),
        conn.getCallModify().error + "");
    for (int i = 0; i < mListeners.size(); i++) {
      mListeners.get(i).onModifyCall(call);
    }
  }

  private void onDisconnect(Connection conn) {
    Log.i(TAG, "onDisconnect");

    mVoicePrivacyState = false;
    final Call call = getCallFromMap(mCallMap, conn, false);

    if (call != null) {
      final boolean wasConferenced = call.getState() == State.CONFERENCED;

      updateCallFromConnection(call, conn, false);

      for (int i = 0; i < mListeners.size(); ++i) {
        mListeners.get(i).onDisconnect(call);
      }

      // If it was a conferenced call, we need to run the entire update
      // to make the proper changes to parent conference calls.
      if (wasConferenced) {
        onPhoneStateChanged(null);
      }

      mCallMap.remove(conn);
    }

    if (MSimTelephonyManager.getDefault().isMultiSimEnabled() && (call != null)) {
      mCallManager.clearDisconnected(call.getSubscription());
    } else {
      mCallManager.clearDisconnected();
    }
    PhoneGlobals.getInstance().updateWakeState();
  }

  private void onSuppServiceNotification(AsyncResult r) {
    SuppServiceNotification notification = (SuppServiceNotification) r.result;
    Phone gsmPhone = PhoneUtils.getGsmPhone(mCallManager);

    Log.d(TAG, "SS Notification: " + notification);

    if (notification.notificationType == SuppServiceNotification.NOTIFICATION_TYPE_MT) {
      if (notification.code == SuppServiceNotification.MT_CODE_FORWARDED_CALL
          || notification.code == SuppServiceNotification.MT_CODE_DEFLECTED_CALL) {
        com.android.internal.telephony.Call ringing = gsmPhone.getRingingCall();
        if (ringing.getState().isRinging()) {
          final Call call = getCallForEarliestConnection(ringing);
          if (call != null) {
            call.setForwarded(true);
            notifyUpdateListeners(call);
          }
        } else {
          mNextGsmCallIsForwarded = true;
        }
      } else if (notification.code == SuppServiceNotification.MT_CODE_CALL_ON_HOLD
          || notification.code == SuppServiceNotification.MT_CODE_CALL_RETRIEVED) {
        final Call call = getCallForEarliestConnection(gsmPhone.getForegroundCall());
        if (call != null) {
          boolean nowHeld = notification.code == SuppServiceNotification.MT_CODE_CALL_ON_HOLD;
          call.setHeldRemotely(nowHeld);
          notifyUpdateListeners(call);
        }
      } else if (notification.code == SuppServiceNotification.MT_CODE_ADDITIONAL_CALL_FORWARDED) {
        com.android.internal.telephony.Call fgCall = gsmPhone.getForegroundCall();
        if (fgCall.getState().isAlive()) {
          final Call call = getCallForEarliestConnection(fgCall);
          if (call != null) {
            call.setAdditionalCallForwarded(true);
            notifyUpdateListeners(call);
          }
        }
      }
    } else if (notification.notificationType == SuppServiceNotification.NOTIFICATION_TYPE_MO) {
      if (notification.code == SuppServiceNotification.MO_CODE_CALL_IS_WAITING) {
        com.android.internal.telephony.Call fgCall = gsmPhone.getForegroundCall();
        if (fgCall.getState().isDialing()) {
          final Call call = getCallForEarliestConnection(fgCall);
          if (call != null) {
            call.setDialingIsWaiting(true);
            notifyUpdateListeners(call);
          }
        }
      } else if (notification.code == SuppServiceNotification.MO_CODE_INCOMING_CALLS_BARRED) {
        final Call call = getCallForEarliestConnection(gsmPhone.getForegroundCall());
        if (call != null) {
          call.setRemoteIncomingCallBarringEnabled(true);
          notifyUpdateListeners(call);
        }
      }
    }
  }

  private Call getCallForEarliestConnection(com.android.internal.telephony.Call call) {
    return getCallFromMap(mCallMap, call.getEarliestConnection(), false);
  }

  private void notifyUpdateListeners(Call call) {
    final List<Call> updatedCalls = Lists.newArrayList();
    updatedCalls.add(call);
    for (int i = 0; i < mListeners.size(); ++i) {
      mListeners.get(i).onUpdate(updatedCalls);
    }
  }

  /** Called when the phone state changes. */
  private void onPhoneStateChanged(AsyncResult r) {
    Log.i(TAG, "onPhoneStateChanged: ");
    // csvt state changed, do not update phone UI.
    if (PhoneGlobals.getInstance().isCsvtActive()) {
      Log.d(TAG, "csvt is active, do not update phone UI.");
      return;
    }
    final List<Call> updatedCalls = Lists.newArrayList();
    doUpdate(false, updatedCalls);

    if (updatedCalls.size() > 0) {
      for (int i = 0; i < mListeners.size(); ++i) {
        mListeners.get(i).onUpdate(updatedCalls);
      }
    }

    PhoneGlobals.getInstance().updateWakeState();
  }

  /**
   * Go through the Calls from CallManager and return the list of calls that were updated. Method
   * also finds any orphaned Calls (Connection objects no longer returned by telephony as either
   * ringing, foreground, or background). For each orphaned call, it sets the call state to IDLE and
   * adds it to the list of calls to update.
   *
   * @param fullUpdate Add all calls to out parameter including those that have no updates.
   * @param out List to populate with Calls that have been updated.
   */
  private void doUpdate(boolean fullUpdate, List<Call> out) {
    final List<com.android.internal.telephony.Call> telephonyCalls = Lists.newArrayList();
    telephonyCalls.addAll(mCallManager.getRingingCalls());
    telephonyCalls.addAll(mCallManager.getForegroundCalls());
    telephonyCalls.addAll(mCallManager.getBackgroundCalls());

    // orphanedConnections starts out including all connections we know about.
    // As we iterate through the connections we get from the telephony layer we
    // prune this Set down to only the connections we have but telephony no longer
    // recognizes.
    final Set<Connection> orphanedConnections = Sets.newHashSet();
    orphanedConnections.addAll(mCallMap.keySet());
    orphanedConnections.addAll(mConfCallMap.keySet());

    // Cycle through all the Connections on all the Calls. Update our Call objects
    // to reflect any new state and send the updated Call objects to the handler service.
    for (com.android.internal.telephony.Call telephonyCall : telephonyCalls) {

      for (Connection connection : telephonyCall.getConnections()) {
        if (DBG) Log.d(TAG, "connection: " + connection + connection.getState());

        if (orphanedConnections.contains(connection)) {
          orphanedConnections.remove(connection);
        }

        // We only send updates for live calls which are not incoming (ringing).
        // Disconnected and incoming calls are handled by onDisconnect and
        // onNewRingingConnection.
        final boolean shouldUpdate =
            connection.getState() != com.android.internal.telephony.Call.State.DISCONNECTED
                && connection.getState() != com.android.internal.telephony.Call.State.IDLE
                && !connection.getState().isRinging();

        final boolean isDisconnecting =
            connection.getState() == com.android.internal.telephony.Call.State.DISCONNECTING;

        // For disconnecting calls, we still need to send the update to the UI but we do
        // not create a new call if the call did not exist.
        final boolean shouldCreate = shouldUpdate && !isDisconnecting;

        // New connections return a Call with INVALID state, which does not translate to
        // a state in the internal.telephony.Call object.  This ensures that staleness
        // check below fails and we always add the item to the update list if it is new.
        final Call call = getCallFromMap(mCallMap, connection, shouldCreate /* create */);

        if (call == null || !shouldUpdate) {
          if (DBG) Log.d(TAG, "update skipped");
          continue;
        }

        boolean changed = updateCallFromConnection(call, connection, false);

        if (fullUpdate || changed) {
          out.add(call);
        }
      }

      // We do a second loop to address conference call scenarios.  We do this as a separate
      // loop to ensure all child calls are up to date before we start updating the parent
      // conference calls.
      for (Connection connection : telephonyCall.getConnections()) {
        updateForConferenceCalls(connection, out);
      }
    }

    // Iterate through orphaned connections, set them to idle, and remove
    // them from our internal structures.
    for (Connection orphanedConnection : orphanedConnections) {
      if (mCallMap.containsKey(orphanedConnection)) {
        final Call call = mCallMap.get(orphanedConnection);
        call.setState(Call.State.IDLE);
        out.add(call);

        mCallMap.remove(orphanedConnection);
      }

      if (mConfCallMap.containsKey(orphanedConnection)) {
        final Call call = mConfCallMap.get(orphanedConnection);
        call.setState(Call.State.IDLE);
        out.add(call);

        mConfCallMap.remove(orphanedConnection);
      }
    }
  }

  /**
   * Checks to see if the connection is the first connection in a conference call. If it is a
   * conference call, we will create a new Conference Call object or update the existing conference
   * call object for that connection. If it is not a conference call but a previous associated
   * conference call still exists, we mark it as idle and remove it from the map. In both cases
   * above, we add the Calls to be updated to the UI.
   *
   * @param connection The connection object to check.
   * @param updatedCalls List of 'updated' calls that will be sent to the UI.
   */
  private boolean updateForConferenceCalls(Connection connection, List<Call> updatedCalls) {
    // We consider this connection a conference connection if the call it
    // belongs to is a multiparty call AND it is the first live connection.
    final boolean isConferenceCallConnection =
        isPartOfLiveConferenceCall(connection)
            && getEarliestLiveConnection(connection.getCall()) == connection;

    boolean changed = false;

    // If this connection is the main connection for the conference call, then create or update
    // a Call object for that conference call.
    if (isConferenceCallConnection) {
      final Call confCall = getCallFromMap(mConfCallMap, connection, true);
      changed = updateCallFromConnection(confCall, connection, true);

      if (changed) {
        updatedCalls.add(confCall);
      }

      if (DBG) Log.d(TAG, "Updating a conference call: " + confCall);

      // It is possible that through a conference call split, there may be lingering conference
      // calls where this connection was the main connection.  We clean those up here.
    } else {
      final Call oldConfCall = getCallFromMap(mConfCallMap, connection, false);

      // We found a conference call for this connection, which is no longer a conference call.
      // Kill it!
      if (oldConfCall != null) {
        if (DBG) Log.d(TAG, "Cleaning up an old conference call: " + oldConfCall);
        mConfCallMap.remove(connection);
        oldConfCall.setState(State.IDLE);
        changed = true;

        // add to the list of calls to update
        updatedCalls.add(oldConfCall);
      }
    }

    return changed;
  }

  private Connection getEarliestLiveConnection(com.android.internal.telephony.Call call) {
    final List<Connection> connections = call.getConnections();
    final int size = connections.size();
    Connection earliestConn = null;
    long earliestTime = Long.MAX_VALUE;
    for (int i = 0; i < size; i++) {
      final Connection connection = connections.get(i);
      if (!connection.isAlive()) continue;
      final long time = connection.getCreateTime();
      if (time < earliestTime) {
        earliestTime = time;
        earliestConn = connection;
      }
    }
    return earliestConn;
  }

  /**
   * Sets the new call state onto the call and performs some additional logic associated with
   * setting the state.
   */
  private void setNewState(Call call, int newState, Connection connection) {
    Preconditions.checkState(call.getState() != newState);

    // When starting an outgoing call, we need to grab gateway information
    // for the call, if available, and set it.
    final RawGatewayInfo info = mCallGatewayManager.getGatewayInfo(connection);

    if (Call.State.isDialing(newState)) {
      if (!info.isEmpty()) {
        call.setGatewayNumber(info.getFormattedGatewayNumber());
        call.setGatewayPackage(info.packageName);
      }
    } else if (!Call.State.isConnected(newState)) {
      mCallGatewayManager.clearGatewayData(connection);
    }

    call.setState(newState);
  }

  private void mapCallDetails(Call call, Connection connection) {
    copyDetails(connection.getCallDetails(), call.getCallDetails(), connection.errorInfo);

    if (connection.getCallModify() != null) {
      copyDetails(
          connection.getCallModify().call_details,
          call.getCallModifyDetails(),
          connection.errorInfo);
    }

    if (connection.getCall().getConfUriList() != null) {
      String[] confList = connection.getCall().getConfUriList();
      call.getCallDetails().setConfUriList(confList);
    }

    call.getCallDetails().setMpty(PhoneUtils.isConferenceCall(connection.getCall()));
  }

  /**
   * copy CallDetails of connection to CallDetails of Call
   *
   * @param src
   * @param dest
   * @param errorInfo
   */
  private void copyDetails(
      com.android.internal.telephony.CallDetails src,
      com.android.services.telephony.common.CallDetails dest,
      String errorInfo) {
    dest.setCallType(src.call_type);
    dest.setCallDomain(src.call_domain);
    dest.setExtras(src.extras);
    dest.setErrorInfo(errorInfo);
    dest.setVideoPauseState(src.getVideoPauseState());
  }

  /**
   * Updates the Call properties to match the state of the connection object that it represents.
   *
   * @param call The call object to update.
   * @param connection The connection object from which to update call.
   * @param isForConference There are slight differences in how we populate data for conference
   *     calls. This boolean tells us which method to use.
   */
  private boolean updateCallFromConnection(
      Call call, Connection connection, boolean isForConference) {
    boolean changed = false;

    final int newState = translateStateFromTelephony(connection, isForConference);

    if (call.getState() != newState) {
      setNewState(call, newState, connection);
      changed = true;
    }

    mapCallDetails(call, connection);

    final Call.DisconnectCause newDisconnectCause =
        translateDisconnectCauseFromTelephony(connection.getDisconnectCause());
    if (call.getDisconnectCause() != newDisconnectCause) {
      call.setDisconnectCause(newDisconnectCause);
      changed = true;
    }

    final long oldConnectTime = call.getConnectTime();
    if (oldConnectTime != connection.getConnectTime()) {
      call.setConnectTime(connection.getConnectTime());
      changed = true;
    }

    // creation time should be fixed
    call.setCreateTime(connection.getCreateTime());

    if (!isForConference) {
      // Number
      final String oldNumber = call.getNumber();
      String newNumber = connection.getAddress();
      RawGatewayInfo info = mCallGatewayManager.getGatewayInfo(connection);
      if (!info.isEmpty()) {
        newNumber = info.trueNumber;
      }
      if (TextUtils.isEmpty(oldNumber) || !oldNumber.equals(newNumber)) {
        call.setNumber(newNumber);
        changed = true;
      }

      // Number presentation
      final int newNumberPresentation = connection.getNumberPresentation();
      if (call.getNumberPresentation() != newNumberPresentation) {
        call.setNumberPresentation(newNumberPresentation);
        changed = true;
      }

      // Name
      final String oldCnapName = call.getCnapName();
      if (TextUtils.isEmpty(oldCnapName) || !oldCnapName.equals(connection.getCnapName())) {
        call.setCnapName(connection.getCnapName());
        changed = true;
      }

      // Name Presentation
      final int newCnapNamePresentation = connection.getCnapNamePresentation();
      if (call.getCnapNamePresentation() != newCnapNamePresentation) {
        call.setCnapNamePresentation(newCnapNamePresentation);
        changed = true;
      }
    } else {

      // update the list of children by:
      // 1) Saving the old set
      // 2) Removing all children
      // 3) Adding the correct children into the Call
      // 4) Comparing the new children set with the old children set
      ImmutableSortedSet<Integer> oldSet = call.getChildCallIds();
      call.removeAllChildren();

      if (connection.getCall() != null) {
        for (Connection childConn : connection.getCall().getConnections()) {
          final Call childCall = getCallFromMap(mCallMap, childConn, false);
          if (childCall != null && childConn.isAlive()) {
            call.addChildId(childCall.getCallId());
          }
        }
      }
      changed |= !oldSet.equals(call.getChildCallIds());
    }

    // Subscription id, this shall be done when Call object created.
    if (call.getSubscription() == MSimConstants.INVALID_SUBSCRIPTION) {
      call.setSubscription(connection.getCall().getPhone().getSubscription());
    }

    /** !!! Uses values from connection and call collected above so this part must be last !!! */
    final int newCapabilities = getCapabilitiesFor(connection, call, isForConference);
    if (call.getCapabilities() != newCapabilities) {
      call.setCapabilities(newCapabilities);
      changed = true;
    }

    return changed;
  }

  /** Returns a mask of capabilities for the connection such as merge, hold, etc. */
  private int getCapabilitiesFor(Connection connection, Call call, boolean isForConference) {
    final boolean callIsActive = (call.getState() == Call.State.ACTIVE);
    final boolean callIsBackground = (call.getState() == Call.State.ONHOLD);
    final Phone phone = connection.getCall().getPhone();

    boolean canAddCall = false;
    boolean canMergeCall = false;
    boolean canSwapCall = false;
    boolean canRespondViaText = false;
    boolean canMute = false;
    boolean canAddParticipant = false;
    boolean canModifyCall = false;
    boolean voicePrivacy = false;
    final boolean supportHold;
    final boolean canHold;

    final boolean genericConf =
        isForConference
            && (connection.getCall().getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA);
    if (!MSimTelephonyManager.getDefault().isMultiSimEnabled()) {
      supportHold = PhoneUtils.okToSupportHold(mCallManager);
      canHold = (supportHold ? PhoneUtils.okToHoldCall(mCallManager) : false);

      // only applies to active calls
      if (callIsActive) {
        canMergeCall = PhoneUtils.okToMergeCalls(mCallManager);
        canSwapCall = PhoneUtils.okToSwapCalls(mCallManager);
      }
      canAddCall = PhoneUtils.okToAddCall(mCallManager);
    } else {
      final int subscription = call.getSubscription();
      supportHold = PhoneUtils.okToSupportHold(mCallManager, subscription);
      canHold = (supportHold ? PhoneUtils.okToHoldCall(mCallManager, subscription) : false);

      // only applies to active calls
      if (callIsActive) {
        canMergeCall = PhoneUtils.okToMergeCalls(mCallManager, subscription);
        canSwapCall = PhoneUtils.okToSwapCalls(mCallManager, subscription);
      }
      canAddCall = PhoneUtils.okToAddCall(mCallManager, subscription);
    }
    if (callIsActive || callIsBackground) {
      canModifyCall = PhoneUtils.isVTModifyAllowed(connection);
    }
    canAddParticipant = PhoneUtils.canAddParticipant(mCallManager) && canAddCall;

    // "Mute": only enabled when the foreground call is ACTIVE.
    // (It's meaningless while on hold, or while DIALING/ALERTING.)
    // It's also explicitly disabled during emergency calls or if
    // emergency callback mode (ECM) is active.
    boolean isEmergencyCall = false;
    if (connection != null) {
      isEmergencyCall =
          PhoneNumberUtils.isLocalEmergencyNumber(connection.getAddress(), phone.getContext());
    }
    boolean isECM = PhoneUtils.isPhoneInEcm(phone);
    if (isEmergencyCall || isECM) { // disable "Mute" item
      canMute = false;
    } else {
      canMute = callIsActive;
    }

    canRespondViaText = RejectWithTextMessageManager.allowRespondViaSmsForCall(call, connection);

    // special rules section!
    // CDMA always has Add
    if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
      canAddCall = true;
    }

    // Voice Privacy for CDMA
    if ((phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) && mVoicePrivacyState) {
      voicePrivacy = true;
    }

    int retval = 0x0;
    if (canHold) {
      retval |= Capabilities.HOLD;
    }
    if (supportHold) {
      retval |= Capabilities.SUPPORT_HOLD;
    }
    if (canAddCall) {
      retval |= Capabilities.ADD_CALL;
    }
    if (canMergeCall) {
      retval |= Capabilities.MERGE_CALLS;
    }
    if (canSwapCall) {
      retval |= Capabilities.SWAP_CALLS;
    }
    if (canRespondViaText) {
      retval |= Capabilities.RESPOND_VIA_TEXT;
    }
    if (canMute) {
      retval |= Capabilities.MUTE;
    }
    if (canAddParticipant) {
      retval |= Capabilities.ADD_PARTICIPANT;
    }
    if (genericConf) {
      retval |= Capabilities.GENERIC_CONFERENCE;
    }
    if (canModifyCall) {
      retval |= Capabilities.MODIFY_CALL;
    }
    if (voicePrivacy) {
      retval |= Capabilities.VOICE_PRIVACY;
    }
    return retval;
  }

  /**
   * Returns true if the Connection is part of a multiparty call. We do this by checking the
   * isMultiparty() method of the telephony.Call object and also checking to see if more than one of
   * it's children is alive.
   */
  private boolean isPartOfLiveConferenceCall(Connection connection) {
    boolean ret = false;
    if (connection.getCall() != null && connection.getCall().isMultiparty()) {
      int count = 0;
      if (connection.getCallDetails().call_domain
          == com.android.services.telephony.common.CallDetails.CALL_DOMAIN_PS) {
        ret = true;
      } else {
        for (Connection currConn : connection.getCall().getConnections()) {
          // Only count connections which are alive and never cound
          // the special
          // "dialing" 3way call for CDMA calls.
          if (currConn.isAlive() && currConn != mCdmaOutgoingConnection) {
            count++;
            if (count >= 2) {
              return true;
            }
          }
        }
      }
    }
    return ret;
  }

  private int translateStateFromTelephony(Connection connection, boolean isForConference) {

    com.android.internal.telephony.Call.State connState = connection.getState();

    // For the "fake" outgoing CDMA call, we need to always treat it as an outgoing call.
    if (mCdmaOutgoingConnection == connection) {
      connState = com.android.internal.telephony.Call.State.DIALING;
    }

    int retval = State.IDLE;
    switch (connState) {
      case ACTIVE:
        retval = State.ACTIVE;
        break;
      case INCOMING:
        retval = State.INCOMING;
        break;
      case DIALING:
      case ALERTING:
        if (PhoneGlobals.getInstance().notifier.getIsCdmaRedialCall()) {
          retval = State.REDIALING;
        } else {
          retval = State.DIALING;
        }
        break;
      case WAITING:
        retval = State.CALL_WAITING;
        break;
      case HOLDING:
        retval = State.ONHOLD;
        break;
      case DISCONNECTING:
        retval = State.DISCONNECTING;
        break;
      case DISCONNECTED:
        retval = State.DISCONNECTED;
      default:
    }

    // If we are dealing with a potential child call (not the parent conference call),
    // the check to see if we have to set the state to CONFERENCED.
    if (!isForConference) {
      // if the connection is part of a multiparty call, and it is live,
      // annotate it with CONFERENCED state instead.
      if (isPartOfLiveConferenceCall(connection) && connection.isAlive()) {
        return State.CONFERENCED;
      }
    }

    return retval;
  }

  /** Called when the active subscription changes. */
  private void onActiveSubChanged(AsyncResult r) {
    int activeSub = (Integer) r.result;
    Log.i(TAG, "onActiveSubChanged: " + activeSub);

    for (int i = 0; i < mListeners.size(); ++i) {
      mListeners.get(i).onActiveSubChanged(activeSub);
    }
  }

  private final ImmutableMap<Connection.DisconnectCause, Call.DisconnectCause> CAUSE_MAP =
      ImmutableMap.<Connection.DisconnectCause, Call.DisconnectCause>builder()
          .put(Connection.DisconnectCause.BUSY, Call.DisconnectCause.BUSY)
          .put(Connection.DisconnectCause.CALL_BARRED, Call.DisconnectCause.CALL_BARRED)
          .put(
              Connection.DisconnectCause.CDMA_ACCESS_BLOCKED,
              Call.DisconnectCause.CDMA_ACCESS_BLOCKED)
          .put(
              Connection.DisconnectCause.CDMA_ACCESS_FAILURE,
              Call.DisconnectCause.CDMA_ACCESS_FAILURE)
          .put(Connection.DisconnectCause.CDMA_DROP, Call.DisconnectCause.CDMA_DROP)
          .put(Connection.DisconnectCause.CDMA_INTERCEPT, Call.DisconnectCause.CDMA_INTERCEPT)
          .put(
              Connection.DisconnectCause.CDMA_LOCKED_UNTIL_POWER_CYCLE,
              Call.DisconnectCause.CDMA_LOCKED_UNTIL_POWER_CYCLE)
          .put(
              Connection.DisconnectCause.CDMA_NOT_EMERGENCY,
              Call.DisconnectCause.CDMA_NOT_EMERGENCY)
          .put(Connection.DisconnectCause.CDMA_PREEMPTED, Call.DisconnectCause.CDMA_PREEMPTED)
          .put(Connection.DisconnectCause.CDMA_REORDER, Call.DisconnectCause.CDMA_REORDER)
          .put(Connection.DisconnectCause.CDMA_RETRY_ORDER, Call.DisconnectCause.CDMA_RETRY_ORDER)
          .put(Connection.DisconnectCause.CDMA_SO_REJECT, Call.DisconnectCause.CDMA_SO_REJECT)
          .put(Connection.DisconnectCause.CONGESTION, Call.DisconnectCause.CONGESTION)
          .put(Connection.DisconnectCause.CS_RESTRICTED, Call.DisconnectCause.CS_RESTRICTED)
          .put(
              Connection.DisconnectCause.CS_RESTRICTED_EMERGENCY,
              Call.DisconnectCause.CS_RESTRICTED_EMERGENCY)
          .put(
              Connection.DisconnectCause.CS_RESTRICTED_NORMAL,
              Call.DisconnectCause.CS_RESTRICTED_NORMAL)
          .put(Connection.DisconnectCause.ERROR_UNSPECIFIED, Call.DisconnectCause.ERROR_UNSPECIFIED)
          .put(Connection.DisconnectCause.FDN_BLOCKED, Call.DisconnectCause.FDN_BLOCKED)
          .put(Connection.DisconnectCause.ICC_ERROR, Call.DisconnectCause.ICC_ERROR)
          .put(Connection.DisconnectCause.INCOMING_MISSED, Call.DisconnectCause.INCOMING_MISSED)
          .put(Connection.DisconnectCause.INCOMING_REJECTED, Call.DisconnectCause.INCOMING_REJECTED)
          .put(
              Connection.DisconnectCause.INVALID_CREDENTIALS,
              Call.DisconnectCause.INVALID_CREDENTIALS)
          .put(Connection.DisconnectCause.INVALID_NUMBER, Call.DisconnectCause.INVALID_NUMBER)
          .put(Connection.DisconnectCause.LIMIT_EXCEEDED, Call.DisconnectCause.LIMIT_EXCEEDED)
          .put(Connection.DisconnectCause.LOCAL, Call.DisconnectCause.LOCAL)
          .put(Connection.DisconnectCause.LOST_SIGNAL, Call.DisconnectCause.LOST_SIGNAL)
          .put(Connection.DisconnectCause.MMI, Call.DisconnectCause.MMI)
          .put(Connection.DisconnectCause.NORMAL, Call.DisconnectCause.NORMAL)
          .put(Connection.DisconnectCause.NOT_DISCONNECTED, Call.DisconnectCause.NOT_DISCONNECTED)
          .put(
              Connection.DisconnectCause.NUMBER_UNREACHABLE,
              Call.DisconnectCause.NUMBER_UNREACHABLE)
          .put(Connection.DisconnectCause.OUT_OF_NETWORK, Call.DisconnectCause.OUT_OF_NETWORK)
          .put(Connection.DisconnectCause.OUT_OF_SERVICE, Call.DisconnectCause.OUT_OF_SERVICE)
          .put(Connection.DisconnectCause.POWER_OFF, Call.DisconnectCause.POWER_OFF)
          .put(Connection.DisconnectCause.SERVER_ERROR, Call.DisconnectCause.SERVER_ERROR)
          .put(
              Connection.DisconnectCause.SERVER_UNREACHABLE,
              Call.DisconnectCause.SERVER_UNREACHABLE)
          .put(Connection.DisconnectCause.TIMED_OUT, Call.DisconnectCause.TIMED_OUT)
          .put(
              Connection.DisconnectCause.UNOBTAINABLE_NUMBER,
              Call.DisconnectCause.UNOBTAINABLE_NUMBER)
          .put(
              Connection.DisconnectCause.DIAL_MODIFIED_TO_USSD,
              Call.DisconnectCause.DIAL_MODIFIED_TO_USSD)
          .put(
              Connection.DisconnectCause.DIAL_MODIFIED_TO_SS,
              Call.DisconnectCause.DIAL_MODIFIED_TO_SS)
          .put(
              Connection.DisconnectCause.DIAL_MODIFIED_TO_DIAL,
              Call.DisconnectCause.DIAL_MODIFIED_TO_DIAL)
          .put(Connection.DisconnectCause.SRVCC_CALL_DROP, Call.DisconnectCause.SRVCC_CALL_DROP)
          .put(
              Connection.DisconnectCause.ANSWERED_ELSEWHERE,
              Call.DisconnectCause.ANSWERED_ELSEWHERE)
          .put(Connection.DisconnectCause.CALL_FAIL_MISC, Call.DisconnectCause.CALL_FAIL_MISC)
          .build();

  private Call.DisconnectCause translateDisconnectCauseFromTelephony(
      Connection.DisconnectCause causeSource) {

    if (CAUSE_MAP.containsKey(causeSource)) {
      return CAUSE_MAP.get(causeSource);
    }

    return Call.DisconnectCause.UNKNOWN;
  }

  /**
   * Gets an existing callId for a connection, or creates one if none exists. This function does NOT
   * set any of the Connection data onto the Call class. A separate call to updateCallFromConnection
   * must be made for that purpose.
   */
  private Call getCallFromMap(
      HashMap<Connection, Call> map, Connection conn, boolean createIfMissing) {
    Call call = null;

    // Find the call id or create if missing and requested.
    if (conn != null) {
      if (map.containsKey(conn)) {
        call = map.get(conn);
      } else if (createIfMissing) {
        call = createNewCall();
        map.put(conn, call);
      }
    }
    return call;
  }

  /** Creates a brand new connection for the call. */
  private Call createNewCall() {
    int callId;
    int newNextCallId;
    do {
      callId = mNextCallId.get();

      // protect against overflow
      newNextCallId = (callId == Integer.MAX_VALUE ? CALL_ID_START_VALUE : callId + 1);

      // Keep looping if the change was not atomic OR the value is already taken.
      // The call to containsValue() is linear, however, most devices support a
      // maximum of 7 connections so it's not expensive.
    } while (!mNextCallId.compareAndSet(callId, newNextCallId));

    return new Call(callId);
  }

  /** Listener interface for changes to Calls. */
  public interface Listener {
    void onDisconnect(Call call);

    void onIncoming(Call call);

    void onUpdate(List<Call> calls);

    void onPostDialAction(
        Connection.PostDialState state, int callId, String remainingChars, char c);

    void onActiveSubChanged(int activeSub);

    void onModifyCall(Call call);

    void onSuppServiceFailed(int service);
  }

  /** Result class for accessing a call by connection. */
  public static class CallResult {
    public Call mCall;
    public Call mActionableCall;
    public Connection mConnection;

    private CallResult(Call call, Connection connection) {
      this(call, call, connection);
    }

    private CallResult(Call call, Call actionableCall, Connection connection) {
      mCall = call;
      mActionableCall = actionableCall;
      mConnection = connection;
    }

    public Call getCall() {
      return mCall;
    }

    // The call that should be used for call actions like hanging up.
    public Call getActionableCall() {
      return mActionableCall;
    }

    public Connection getConnection() {
      return mConnection;
    }
  }
}
/**
 * Controls and utilities for low-level {@code init} services.
 *
 * @hide
 */
public class SystemService {

  private static HashMap<String, State> sStates = Maps.newHashMap();

  /** State of a known {@code init} service. */
  public enum State {
    RUNNING("running"),
    STOPPING("stopping"),
    STOPPED("stopped"),
    RESTARTING("restarting");

    State(String state) {
      sStates.put(state, this);
    }
  }

  private static Object sPropertyLock = new Object();

  static {
    SystemProperties.addChangeCallback(
        new Runnable() {
          @Override
          public void run() {
            synchronized (sPropertyLock) {
              sPropertyLock.notifyAll();
            }
          }
        });
  }

  /** Request that the init daemon start a named service. */
  public static void start(String name) {
    SystemProperties.set("ctl.start", name);
  }

  /** Request that the init daemon stop a named service. */
  public static void stop(String name) {
    SystemProperties.set("ctl.stop", name);
  }

  /** Request that the init daemon restart a named service. */
  public static void restart(String name) {
    SystemProperties.set("ctl.restart", name);
  }

  /** Return current state of given service. */
  public static State getState(String service) {
    final String rawState = SystemProperties.get("init.svc." + service);
    final State state = sStates.get(rawState);
    if (state != null) {
      return state;
    } else {
      return State.STOPPED;
    }
  }

  /** Check if given service is {@link State#STOPPED}. */
  public static boolean isStopped(String service) {
    return State.STOPPED.equals(getState(service));
  }

  /** Check if given service is {@link State#RUNNING}. */
  public static boolean isRunning(String service) {
    return State.RUNNING.equals(getState(service));
  }

  /** Wait until given service has entered specific state. */
  public static void waitForState(String service, State state, long timeoutMillis)
      throws TimeoutException {
    final long endMillis = SystemClock.elapsedRealtime() + timeoutMillis;
    while (true) {
      synchronized (sPropertyLock) {
        final State currentState = getState(service);
        if (state.equals(currentState)) {
          return;
        }

        if (SystemClock.elapsedRealtime() >= endMillis) {
          throw new TimeoutException(
              "Service "
                  + service
                  + " currently "
                  + currentState
                  + "; waited "
                  + timeoutMillis
                  + "ms for "
                  + state);
        }

        try {
          sPropertyLock.wait(timeoutMillis);
        } catch (InterruptedException e) {
        }
      }
    }
  }

  /** Wait until any of given services enters {@link State#STOPPED}. */
  public static void waitForAnyStopped(String... services) {
    while (true) {
      synchronized (sPropertyLock) {
        for (String service : services) {
          if (State.STOPPED.equals(getState(service))) {
            return;
          }
        }

        try {
          sPropertyLock.wait();
        } catch (InterruptedException e) {
        }
      }
    }
  }
}
Example #4
0
public class RecentLoader extends AsyncTaskLoader<DirectoryResult> {
  private static final boolean LOGD = true;

  // TODO: clean up cursor ownership so background thread doesn't traverse
  // previously returned cursors for filtering/sorting; this currently races
  // with the UI thread.

  private static final int MAX_OUTSTANDING_RECENTS = 4;
  private static final int MAX_OUTSTANDING_RECENTS_SVELTE = 2;

  /** Time to wait for first pass to complete before returning partial results. */
  private static final int MAX_FIRST_PASS_WAIT_MILLIS = 500;

  /** Maximum documents from a single root. */
  private static final int MAX_DOCS_FROM_ROOT = 64;

  /** Ignore documents older than this age. */
  private static final long REJECT_OLDER_THAN = 45 * DateUtils.DAY_IN_MILLIS;

  /** MIME types that should always be excluded from recents. */
  private static final String[] RECENT_REJECT_MIMES = new String[] {Document.MIME_TYPE_DIR};

  private final Semaphore mQueryPermits;

  private final RootsCache mRoots;
  private final State mState;

  private final HashMap<RootInfo, RecentTask> mTasks = Maps.newHashMap();

  private final int mSortOrder = State.SORT_ORDER_LAST_MODIFIED;

  private CountDownLatch mFirstPassLatch;
  private volatile boolean mFirstPassDone;

  private DirectoryResult mResult;

  /// M: add to support drm
  private int mDrmLevel;

  /// M: FOR ALPS01480274 @{
  private static final int CONTENT_CHANGE = 1;
  private Handler mUiHandler =
      new Handler(Looper.getMainLooper()) {
        public void handleMessage(Message msg) {
          Log.d(TAG, "onContentChanged");
          onContentChanged();
        }
      };
  /// @}

  // TODO: create better transfer of ownership around cursor to ensure its
  // closed in all edge cases.

  public class RecentTask extends AbstractFuture<Cursor> implements Runnable, Closeable {
    public final String authority;
    public final String rootId;

    private Cursor mWithRoot;

    public RecentTask(String authority, String rootId) {
      this.authority = authority;
      this.rootId = rootId;
    }

    @Override
    public void run() {
      if (isCancelled()) return;

      try {
        mQueryPermits.acquire();
      } catch (InterruptedException e) {
        return;
      }

      try {
        runInternal();
      } finally {
        mQueryPermits.release();
      }
    }

    public void runInternal() {
      ContentProviderClient client = null;
      try {
        client =
            DocumentsApplication.acquireUnstableProviderOrThrow(
                getContext().getContentResolver(), authority);

        final Uri uri = DocumentsContract.buildRecentDocumentsUri(authority, rootId);
        final Cursor cursor =
            client.query(uri, null, null, null, DirectoryLoader.getQuerySortOrder(mSortOrder));
        mWithRoot = new RootCursorWrapper(authority, rootId, cursor, MAX_DOCS_FROM_ROOT);

      } catch (Exception e) {
        Log.w(TAG, "Failed to load " + authority + ", " + rootId, e);
      } finally {
        ContentProviderClient.releaseQuietly(client);
      }

      set(mWithRoot);

      mFirstPassLatch.countDown();
      /// M: FOR ALPS01480274 @{
      if (mFirstPassDone && !mUiHandler.hasMessages(CONTENT_CHANGE)) {
        mUiHandler.sendEmptyMessage(CONTENT_CHANGE);
      }
      /// @}
    }

    @Override
    public void close() throws IOException {
      IoUtils.closeQuietly(mWithRoot);
    }
  }

  public RecentLoader(Context context, RootsCache roots, State state) {
    super(context);
    mRoots = roots;
    mState = state;

    /// M: add to support drm
    mDrmLevel =
        ((DocumentsActivity) context)
            .getIntent()
            .getIntExtra(OmaDrmStore.DrmExtra.EXTRA_DRM_LEVEL, -1);

    // Keep clients around on high-RAM devices, since we'd be spinning them
    // up moments later to fetch thumbnails anyway.
    final ActivityManager am =
        (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
    mQueryPermits =
        new Semaphore(
            am.isLowRamDevice() ? MAX_OUTSTANDING_RECENTS_SVELTE : MAX_OUTSTANDING_RECENTS);
  }

  @Override
  public DirectoryResult loadInBackground() {
    if (mFirstPassLatch == null) {
      // First time through we kick off all the recent tasks, and wait
      // around to see if everyone finishes quickly.

      final Collection<RootInfo> roots = mRoots.getMatchingRootsBlocking(mState);
      for (RootInfo root : roots) {
        if ((root.flags & Root.FLAG_SUPPORTS_RECENTS) != 0) {
          final RecentTask task = new RecentTask(root.authority, root.rootId);
          mTasks.put(root, task);
        }
      }

      mFirstPassLatch = new CountDownLatch(mTasks.size());
      for (RecentTask task : mTasks.values()) {
        ProviderExecutor.forAuthority(task.authority).execute(task);
      }

      try {
        mFirstPassLatch.await(MAX_FIRST_PASS_WAIT_MILLIS, TimeUnit.MILLISECONDS);
        mFirstPassDone = true;
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }

    final long rejectBefore = System.currentTimeMillis() - REJECT_OLDER_THAN;

    // Collect all finished tasks
    boolean allDone = true;
    List<Cursor> cursors = Lists.newArrayList();
    for (RecentTask task : mTasks.values()) {
      if (task.isDone()) {
        try {
          final Cursor cursor = task.get();
          if (cursor == null) continue;

          final FilteringCursorWrapper filtered =
              new FilteringCursorWrapper(
                  cursor, mState.acceptMimes, RECENT_REJECT_MIMES, rejectBefore) {
                @Override
                public void close() {
                  // Ignored, since we manage cursor lifecycle internally
                }
              };
          cursors.add(filtered);
        } catch (InterruptedException e) {
          throw new RuntimeException(e);
        } catch (ExecutionException e) {
          // We already logged on other side
        } catch (IllegalStateException e) {
          /// M: cursor get from task may have been closed, this happen when recent loader have been
          /// reset(cursor will be closed now on main thread) but loader thread still loading(may
          // access
          /// this have been closed cursor), so we need catch this type IllegalStateException.
          Log.w(TAG, "cursor may have been closed when recent loader reset", e);
        }
      } else {
        allDone = false;
      }
    }

    if (LOGD) {
      Log.d(TAG, "Found " + cursors.size() + " of " + mTasks.size() + " recent queries done");
    }

    final DirectoryResult result = new DirectoryResult();
    result.sortOrder = SORT_ORDER_LAST_MODIFIED;

    // Hint to UI if we're still loading
    final Bundle extras = new Bundle();
    if (!allDone) {
      extras.putBoolean(DocumentsContract.EXTRA_LOADING, true);
    }

    final Cursor merged;
    if (cursors.size() > 0) {
      merged = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
    } else {
      // Return something when nobody is ready
      merged = new MatrixCursor(new String[0]);
    }

    /// M: add to support drm, only show these drm files match given drm level.
    final SortingCursorWrapper sorted =
        new SortingCursorWrapper(merged, result.sortOrder, mDrmLevel) {
          @Override
          public Bundle getExtras() {
            return extras;
          }
        };

    result.cursor = sorted;

    return result;
  }

  @Override
  public void cancelLoadInBackground() {
    super.cancelLoadInBackground();
  }

  @Override
  public void deliverResult(DirectoryResult result) {
    if (isReset()) {
      IoUtils.closeQuietly(result);
      return;
    }
    DirectoryResult oldResult = mResult;
    mResult = result;

    if (isStarted()) {
      super.deliverResult(result);
    }

    if (oldResult != null && oldResult != result) {
      IoUtils.closeQuietly(oldResult);
    }
  }

  @Override
  protected void onStartLoading() {
    if (mResult != null) {
      deliverResult(mResult);
    }
    if (takeContentChanged() || mResult == null) {
      forceLoad();
    }
  }

  @Override
  protected void onStopLoading() {
    cancelLoad();
  }

  @Override
  public void onCanceled(DirectoryResult result) {
    IoUtils.closeQuietly(result);
  }

  @Override
  protected void onReset() {
    super.onReset();

    // Ensure the loader is stopped
    onStopLoading();

    for (RecentTask task : mTasks.values()) {
      IoUtils.closeQuietly(task);
    }

    IoUtils.closeQuietly(mResult);
    mResult = null;
  }
}
Example #5
0
/** Cache for common nicknames. */
public class CommonNicknameCache {

  // We will use this much memory (in bits) to optimize the nickname cluster lookup
  private static final int NICKNAME_BLOOM_FILTER_SIZE = 0x1FFF; // =long[128]
  private BitSet mNicknameBloomFilter;

  private HashMap<String, SoftReference<String[]>> mNicknameClusterCache = Maps.newHashMap();

  private final SQLiteDatabase mDb;

  public CommonNicknameCache(SQLiteDatabase db) {
    mDb = db;
  }

  private static final class NicknameLookupPreloadQuery {
    public static final String TABLE = Tables.NICKNAME_LOOKUP;

    public static final String[] COLUMNS = new String[] {NicknameLookupColumns.NAME};

    public static final int NAME = 0;
  }

  /**
   * Read all known common nicknames from the database and populate a Bloom filter using the
   * corresponding hash codes. The idea is to eliminate most of unnecessary database lookups for
   * nicknames. Given a name, we will take its hash code and see if it is set in the Bloom filter.
   * If not, we will know that the name is not in the database. If it is, we still need to run a
   * query.
   *
   * <p>Given the size of the filter and the expected size of the nickname table, we should expect
   * the combination of the Bloom filter and cache will prevent around 98-99% of unnecessary queries
   * from running.
   */
  private void preloadNicknameBloomFilter() {
    mNicknameBloomFilter = new BitSet(NICKNAME_BLOOM_FILTER_SIZE + 1);
    Cursor cursor =
        mDb.query(
            NicknameLookupPreloadQuery.TABLE,
            NicknameLookupPreloadQuery.COLUMNS,
            null,
            null,
            null,
            null,
            null);
    try {
      int count = cursor.getCount();
      for (int i = 0; i < count; i++) {
        cursor.moveToNext();
        String normalizedName = cursor.getString(NicknameLookupPreloadQuery.NAME);
        int hashCode = normalizedName.hashCode();
        mNicknameBloomFilter.set(hashCode & NICKNAME_BLOOM_FILTER_SIZE);
      }
    } finally {
      cursor.close();
    }
  }

  /** Returns nickname cluster IDs or null. Maintains cache. */
  protected String[] getCommonNicknameClusters(String normalizedName) {
    if (mNicknameBloomFilter == null) {
      preloadNicknameBloomFilter();
    }

    int hashCode = normalizedName.hashCode();
    if (!mNicknameBloomFilter.get(hashCode & NICKNAME_BLOOM_FILTER_SIZE)) {
      return null;
    }

    SoftReference<String[]> ref;
    String[] clusters = null;
    synchronized (mNicknameClusterCache) {
      if (mNicknameClusterCache.containsKey(normalizedName)) {
        ref = mNicknameClusterCache.get(normalizedName);
        if (ref == null) {
          return null;
        }
        clusters = ref.get();
      }
    }

    if (clusters == null) {
      clusters = loadNicknameClusters(normalizedName);
      ref = clusters == null ? null : new SoftReference<String[]>(clusters);
      synchronized (mNicknameClusterCache) {
        mNicknameClusterCache.put(normalizedName, ref);
      }
    }
    return clusters;
  }

  private interface NicknameLookupQuery {
    String TABLE = Tables.NICKNAME_LOOKUP;

    String[] COLUMNS = new String[] {NicknameLookupColumns.CLUSTER};

    int CLUSTER = 0;
  }

  protected String[] loadNicknameClusters(String normalizedName) {
    String[] clusters = null;
    Cursor cursor =
        mDb.query(
            NicknameLookupQuery.TABLE,
            NicknameLookupQuery.COLUMNS,
            NicknameLookupColumns.NAME + "=?",
            new String[] {normalizedName},
            null,
            null,
            null);
    try {
      int count = cursor.getCount();
      if (count > 0) {
        clusters = new String[count];
        for (int i = 0; i < count; i++) {
          cursor.moveToNext();
          clusters[i] = cursor.getString(NicknameLookupQuery.CLUSTER);
        }
      }
    } finally {
      cursor.close();
    }
    return clusters;
  }
}