blob: eb7b77a2234f81b2c7dcbb9326a4b4cf147388c8 [file] [log] [blame]
/*
* Copyright (C) 2010 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.provider;
import android.annotation.Nullable;
import android.location.LocationResult;
import android.location.provider.ProviderProperties;
import android.location.provider.ProviderRequest;
import android.location.util.identity.CallerIdentity;
import android.os.Binder;
import android.os.Bundle;
import com.android.internal.util.Preconditions;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.UnaryOperator;
/**
* Base class for all location providers.
*
* @hide
*/
public abstract class AbstractLocationProvider {
/**
* Interface for listening to location providers.
*/
public interface Listener {
/**
* Called when a provider's state changes. May be invoked from any thread. Will be
* invoked with a cleared binder identity.
*/
void onStateChanged(State oldState, State newState);
/**
* Called when a provider has a new location available. May be invoked from any thread. Will
* be invoked with a cleared binder identity.
*/
void onReportLocation(LocationResult locationResult);
}
/**
* Holds a representation of the public state of a provider.
*/
public static final class State {
/**
* Default state value for a location provider that is disabled with no properties and an
* empty extra attribution tag set.
*/
public static final State EMPTY_STATE = new State(false, null, null,
Collections.emptySet());
/**
* The provider's allowed state.
*/
public final boolean allowed;
/**
* The provider's properties.
*/
@Nullable public final ProviderProperties properties;
/**
* The provider's identity - providers may be afforded special privileges.
*/
@Nullable public final CallerIdentity identity;
/**
* A set of attribution tags also associated with this provider - these attribution tags may
* be afforded special privileges.
*/
public final Set<String> extraAttributionTags;
private State(boolean allowed, ProviderProperties properties, CallerIdentity identity,
Set<String> extraAttributionTags) {
this.allowed = allowed;
this.properties = properties;
this.identity = identity;
this.extraAttributionTags = Objects.requireNonNull(extraAttributionTags);
}
/**
* Returns a state the same as the current but with allowed set as specified.
*/
public State withAllowed(boolean allowed) {
if (allowed == this.allowed) {
return this;
} else {
return new State(allowed, properties, identity, extraAttributionTags);
}
}
/**
* Returns a state the same as the current but with properties set as specified.
*/
public State withProperties(@Nullable ProviderProperties properties) {
if (Objects.equals(properties, this.properties)) {
return this;
} else {
return new State(allowed, properties, identity, extraAttributionTags);
}
}
/**
* Returns a state the same as the current but with an identity set as specified.
*/
public State withIdentity(@Nullable CallerIdentity identity) {
if (Objects.equals(identity, this.identity)) {
return this;
} else {
return new State(allowed, properties, identity, extraAttributionTags);
}
}
/**
* Returns a state the same as the current but with extra attribution tags set as specified.
*/
public State withExtraAttributionTags(Set<String> extraAttributionTags) {
if (extraAttributionTags.equals(this.extraAttributionTags)) {
return this;
} else {
return new State(allowed, properties, identity, extraAttributionTags);
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof State)) {
return false;
}
State state = (State) o;
return allowed == state.allowed && properties == state.properties
&& Objects.equals(identity, state.identity)
&& extraAttributionTags.equals(state.extraAttributionTags);
}
@Override
public int hashCode() {
return Objects.hash(allowed, properties, identity, extraAttributionTags);
}
}
// combines listener and state information so that they can be updated atomically with respect
// to each other and an ordering established.
private static class InternalState {
@Nullable public final Listener listener;
public final State state;
InternalState(@Nullable Listener listener, State state) {
this.listener = listener;
this.state = state;
}
InternalState withListener(Listener listener) {
if (listener == this.listener) {
return this;
} else {
return new InternalState(listener, state);
}
}
InternalState withState(State state) {
if (state.equals(this.state)) {
return this;
} else {
return new InternalState(listener, state);
}
}
InternalState withState(UnaryOperator<State> operator) {
return withState(operator.apply(state));
}
}
protected final Executor mExecutor;
// we use a lock-free implementation to update state to ensure atomicity between updating the
// provider state and setting the listener, so that the state updates a listener sees are
// consistent with when the listener was set (a listener should not see any updates that occur
// before it was set, and should not miss any updates that occur after it was set).
private final AtomicReference<InternalState> mInternalState;
private final LocationProviderController mController;
/**
* Creates a new location provider.
*
* All callback methods will be invoked on the given executor. A direct executor may be provided
* only if the provider can guarantee that all callback methods will never synchronously invoke
* any {@link LocationProviderController} methods. If this invariant is not held, use a normal
* executor or risk deadlock.
*
* An optional identity and properties may be provided to initialize the location provider.
*/
protected AbstractLocationProvider(Executor executor, @Nullable CallerIdentity identity,
@Nullable ProviderProperties properties, Set<String> extraAttributionTags) {
Preconditions.checkArgument(identity == null || identity.getListenerId() == null);
mExecutor = Objects.requireNonNull(executor);
mInternalState = new AtomicReference<>(new InternalState(null,
State.EMPTY_STATE
.withIdentity(identity)
.withProperties(properties)
.withExtraAttributionTags(extraAttributionTags)));
mController = new Controller();
}
/**
* Retrieves the controller for this location provider. Should never be invoked by subclasses,
* as a location provider should not be controlling itself. Using this method from subclasses
* could also result in deadlock.
*/
LocationProviderController getController() {
return mController;
}
protected void setState(UnaryOperator<State> operator) {
AtomicReference<State> oldStateRef = new AtomicReference<>();
InternalState newInternalState = mInternalState.updateAndGet(
internalState -> {
oldStateRef.set(internalState.state);
return internalState.withState(operator);
});
State oldState = oldStateRef.get();
if (oldState.equals(newInternalState.state)) {
return;
}
if (newInternalState.listener != null) {
final long identity = Binder.clearCallingIdentity();
try {
newInternalState.listener.onStateChanged(oldState, newInternalState.state);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
/**
* The current state of the provider.
*/
public final State getState() {
return mInternalState.get().state;
}
/**
* Call this method to report a change in provider allowed status.
*/
protected void setAllowed(boolean allowed) {
setState(state -> state.withAllowed(allowed));
}
/**
* Call this method to report a change in provider properties.
*/
protected void setProperties(@Nullable ProviderProperties properties) {
setState(state -> state.withProperties(properties));
}
/**
* Call this method to report a change in the provider's identity.
*/
protected void setIdentity(@Nullable CallerIdentity identity) {
Preconditions.checkArgument(identity == null || identity.getListenerId() == null);
setState(state -> state.withIdentity(identity));
}
public final Set<String> getExtraAttributionTags() {
return mInternalState.get().state.extraAttributionTags;
}
/**
* Call this method to report a change in the provider's extra attribution tags.
*/
protected void setExtraAttributionTags(Set<String> extraAttributionTags) {
setState(state -> state.withExtraAttributionTags(extraAttributionTags));
}
/**
* Call this method to report a new location.
*/
protected void reportLocation(LocationResult locationResult) {
Listener listener = mInternalState.get().listener;
if (listener != null) {
final long identity = Binder.clearCallingIdentity();
try {
listener.onReportLocation(Objects.requireNonNull(locationResult));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
/**
* Callback invoked when the provider is started, and signals that other callback invocations
* can now be expected. Always implies that the provider request is set to the empty request.
* Always invoked on the provider executor.
*/
protected void onStart() { }
/**
* Callback invoked when the provider is stopped, and signals that no further callback
* invocations will occur (until a further call to {@link #onStart()}. Always invoked on the
* provider executor.
*/
protected void onStop() { }
/**
* Callback invoked to inform the provider of a new provider request which replaces any prior
* provider request. Always invoked on the provider executor.
*/
protected abstract void onSetRequest(ProviderRequest request);
/**
* Callback invoked to request any batched locations to be flushed. The argument callback must
* always be invoked exactly once for every invocation of this method, and should only be
* invoked only after {@link #reportLocation(LocationResult)} has been called for all flushed
* locations. If no locations are flushed, the argument callback may be invoked immediately.
* Always invoked on the provider executor.
*/
protected abstract void onFlush(Runnable callback);
/**
* Callback invoked to informs the provider of an extra command it may choose to respond to.
* Always invoked on the provider executor.
*/
protected abstract void onExtraCommand(int uid, int pid, String command, Bundle extras);
/**
* Dumps debug or log information. May be invoked from any thread.
*/
protected abstract void dump(FileDescriptor fd, PrintWriter pw, String[] args);
private class Controller implements LocationProviderController {
private boolean mStarted = false;
Controller() {}
@Override
public State setListener(@Nullable Listener listener) {
InternalState oldInternalState = mInternalState.getAndUpdate(
internalState -> internalState.withListener(listener));
Preconditions.checkState(listener == null || oldInternalState.listener == null);
return oldInternalState.state;
}
@Override
public boolean isStarted() {
return mStarted;
}
@Override
public void start() {
Preconditions.checkState(!mStarted);
mStarted = true;
mExecutor.execute(AbstractLocationProvider.this::onStart);
}
@Override
public void stop() {
Preconditions.checkState(mStarted);
mStarted = false;
mExecutor.execute(AbstractLocationProvider.this::onStop);
}
@Override
public void setRequest(ProviderRequest request) {
Preconditions.checkState(mStarted);
mExecutor.execute(() -> onSetRequest(request));
}
@Override
public void flush(Runnable listener) {
Preconditions.checkState(mStarted);
mExecutor.execute(() -> onFlush(listener));
}
@Override
public void sendExtraCommand(int uid, int pid, String command, Bundle extras) {
Preconditions.checkState(mStarted);
mExecutor.execute(() -> onExtraCommand(uid, pid, command, extras));
}
}
}