Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2010 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.android.server.location; |
| 18 | |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 19 | import android.annotation.Nullable; |
Soonil Nagarkar | 0d77ea6 | 2019-01-31 14:36:56 -0800 | [diff] [blame] | 20 | import android.content.Context; |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 21 | import android.location.Location; |
Soonil Nagarkar | 747cbec | 2019-09-19 14:27:03 -0700 | [diff] [blame] | 22 | import android.os.Binder; |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 23 | import android.os.Bundle; |
Soonil Nagarkar | b6375a4 | 2020-01-29 15:23:06 -0800 | [diff] [blame] | 24 | import android.util.ArraySet; |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 25 | |
| 26 | import com.android.internal.location.ProviderProperties; |
| 27 | import com.android.internal.location.ProviderRequest; |
| 28 | |
| 29 | import java.io.FileDescriptor; |
| 30 | import java.io.PrintWriter; |
Soonil Nagarkar | 0d77ea6 | 2019-01-31 14:36:56 -0800 | [diff] [blame] | 31 | import java.util.Collections; |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 32 | import java.util.List; |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 33 | import java.util.Objects; |
| 34 | import java.util.Set; |
| 35 | import java.util.concurrent.Executor; |
| 36 | import java.util.concurrent.atomic.AtomicReference; |
| 37 | import java.util.function.UnaryOperator; |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 38 | |
| 39 | /** |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 40 | * Base class for all location providers. |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 41 | * |
| 42 | * @hide |
| 43 | */ |
| 44 | public abstract class AbstractLocationProvider { |
| 45 | |
| 46 | /** |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 47 | * Interface for listening to location providers. |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 48 | */ |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 49 | public interface Listener { |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 50 | |
| 51 | /** |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 52 | * Called when a provider's state changes. May be invoked from any thread. Will be |
| 53 | * invoked with a cleared binder identity. |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 54 | */ |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 55 | void onStateChanged(State oldState, State newState); |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 56 | |
| 57 | /** |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 58 | * Called when a provider has a new location available. May be invoked from any thread. Will |
| 59 | * be invoked with a cleared binder identity. |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 60 | */ |
| 61 | void onReportLocation(Location location); |
| 62 | |
| 63 | /** |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 64 | * Called when a provider has a new location available. May be invoked from any thread. Will |
| 65 | * be invoked with a cleared binder identity. |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 66 | */ |
| 67 | void onReportLocation(List<Location> locations); |
| 68 | } |
| 69 | |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 70 | /** |
| 71 | * Holds a representation of the public state of a provider. |
| 72 | */ |
| 73 | public static final class State { |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 74 | |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 75 | /** |
| 76 | * Default state value for a location provider that is disabled with no properties and an |
| 77 | * empty provider package list. |
| 78 | */ |
| 79 | public static final State EMPTY_STATE = new State(false, null, |
| 80 | Collections.emptySet()); |
| 81 | |
| 82 | /** |
Soonil Nagarkar | 980ce6a | 2020-01-23 18:06:31 -0800 | [diff] [blame] | 83 | * The provider's allowed state. |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 84 | */ |
Soonil Nagarkar | 980ce6a | 2020-01-23 18:06:31 -0800 | [diff] [blame] | 85 | public final boolean allowed; |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 86 | |
| 87 | /** |
| 88 | * The provider's properties. |
| 89 | */ |
| 90 | @Nullable public final ProviderProperties properties; |
| 91 | |
| 92 | /** |
| 93 | * The provider's package name list - provider packages may be afforded special privileges. |
| 94 | */ |
| 95 | public final Set<String> providerPackageNames; |
| 96 | |
Soonil Nagarkar | 980ce6a | 2020-01-23 18:06:31 -0800 | [diff] [blame] | 97 | private State(boolean allowed, ProviderProperties properties, |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 98 | Set<String> providerPackageNames) { |
Soonil Nagarkar | 980ce6a | 2020-01-23 18:06:31 -0800 | [diff] [blame] | 99 | this.allowed = allowed; |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 100 | this.properties = properties; |
| 101 | this.providerPackageNames = Objects.requireNonNull(providerPackageNames); |
| 102 | } |
| 103 | |
Soonil Nagarkar | 980ce6a | 2020-01-23 18:06:31 -0800 | [diff] [blame] | 104 | private State withAllowed(boolean allowed) { |
| 105 | if (allowed == this.allowed) { |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 106 | return this; |
| 107 | } else { |
Soonil Nagarkar | 980ce6a | 2020-01-23 18:06:31 -0800 | [diff] [blame] | 108 | return new State(allowed, properties, providerPackageNames); |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 109 | } |
| 110 | } |
| 111 | |
| 112 | private State withProperties(ProviderProperties properties) { |
| 113 | if (properties.equals(this.properties)) { |
| 114 | return this; |
| 115 | } else { |
Soonil Nagarkar | 980ce6a | 2020-01-23 18:06:31 -0800 | [diff] [blame] | 116 | return new State(allowed, properties, providerPackageNames); |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 117 | } |
| 118 | } |
| 119 | |
| 120 | private State withProviderPackageNames(Set<String> providerPackageNames) { |
| 121 | if (providerPackageNames.equals(this.providerPackageNames)) { |
| 122 | return this; |
| 123 | } else { |
Soonil Nagarkar | b6375a4 | 2020-01-29 15:23:06 -0800 | [diff] [blame] | 124 | return new State(allowed, properties, |
| 125 | Collections.unmodifiableSet(new ArraySet<>(providerPackageNames))); |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 126 | } |
| 127 | } |
| 128 | |
| 129 | @Override |
| 130 | public boolean equals(Object o) { |
| 131 | if (this == o) { |
| 132 | return true; |
| 133 | } |
| 134 | if (!(o instanceof State)) { |
| 135 | return false; |
| 136 | } |
| 137 | State state = (State) o; |
Soonil Nagarkar | 980ce6a | 2020-01-23 18:06:31 -0800 | [diff] [blame] | 138 | return allowed == state.allowed && properties == state.properties |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 139 | && providerPackageNames.equals(state.providerPackageNames); |
| 140 | } |
| 141 | |
| 142 | @Override |
| 143 | public int hashCode() { |
Soonil Nagarkar | 980ce6a | 2020-01-23 18:06:31 -0800 | [diff] [blame] | 144 | return Objects.hash(allowed, properties, providerPackageNames); |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 145 | } |
| 146 | } |
| 147 | |
| 148 | // combines listener and state information so that they can be updated atomically with respect |
| 149 | // to each other and an ordering established. |
| 150 | private static class InternalState { |
| 151 | @Nullable public final Listener listener; |
| 152 | public final State state; |
| 153 | |
| 154 | private InternalState(@Nullable Listener listener, State state) { |
| 155 | this.listener = listener; |
| 156 | this.state = state; |
| 157 | } |
| 158 | |
| 159 | private InternalState withListener(Listener listener) { |
| 160 | if (listener == this.listener) { |
| 161 | return this; |
| 162 | } else { |
| 163 | return new InternalState(listener, state); |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | private InternalState withState(State state) { |
| 168 | if (state.equals(this.state)) { |
| 169 | return this; |
| 170 | } else { |
| 171 | return new InternalState(listener, state); |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | private InternalState withState(UnaryOperator<State> operator) { |
| 176 | return withState(operator.apply(state)); |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | protected final Context mContext; |
| 181 | protected final Executor mExecutor; |
| 182 | |
| 183 | // we use a lock-free implementation to update state to ensure atomicity between updating the |
| 184 | // provider state and setting the listener, so that the state updates a listener sees are |
| 185 | // consistent with when the listener was set (a listener should not see any updates that occur |
| 186 | // before it was set, and should not miss any updates that occur after it was set). |
| 187 | private final AtomicReference<InternalState> mInternalState; |
| 188 | |
| 189 | protected AbstractLocationProvider(Context context, Executor executor) { |
| 190 | this(context, executor, Collections.singleton(context.getPackageName())); |
| 191 | } |
| 192 | |
| 193 | protected AbstractLocationProvider(Context context, Executor executor, |
| 194 | Set<String> packageNames) { |
Soonil Nagarkar | 0d77ea6 | 2019-01-31 14:36:56 -0800 | [diff] [blame] | 195 | mContext = context; |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 196 | mExecutor = executor; |
| 197 | mInternalState = new AtomicReference<>( |
| 198 | new InternalState(null, State.EMPTY_STATE.withProviderPackageNames(packageNames))); |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 199 | } |
| 200 | |
| 201 | /** |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 202 | * Sets the listener and returns the state at the moment the listener was set. The listener can |
| 203 | * expect to receive all state updates from after this point. |
| 204 | */ |
| 205 | State setListener(@Nullable Listener listener) { |
| 206 | return mInternalState.updateAndGet( |
| 207 | internalState -> internalState.withListener(listener)).state; |
| 208 | } |
| 209 | |
| 210 | /** |
| 211 | * Retrieves the state of the provider. |
| 212 | */ |
| 213 | State getState() { |
| 214 | return mInternalState.get().state; |
| 215 | } |
| 216 | |
| 217 | /** |
| 218 | * Sets the state of the provider to the new state. |
| 219 | */ |
| 220 | void setState(State newState) { |
| 221 | InternalState oldInternalState = mInternalState.getAndUpdate( |
| 222 | internalState -> internalState.withState(newState)); |
| 223 | if (newState.equals(oldInternalState.state)) { |
| 224 | return; |
| 225 | } |
| 226 | |
| 227 | // we know that we only updated the state, so the listener for the old state is the same as |
| 228 | // the listener for the new state. |
| 229 | if (oldInternalState.listener != null) { |
| 230 | long identity = Binder.clearCallingIdentity(); |
| 231 | try { |
| 232 | oldInternalState.listener.onStateChanged(oldInternalState.state, newState); |
| 233 | } finally { |
| 234 | Binder.restoreCallingIdentity(identity); |
| 235 | } |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | private void setState(UnaryOperator<State> operator) { |
| 240 | InternalState oldInternalState = mInternalState.getAndUpdate( |
| 241 | internalState -> internalState.withState(operator)); |
| 242 | |
| 243 | // recreate the new state from our knowledge of the old state - unfortunately may result in |
| 244 | // an extra allocation, but oh well... |
| 245 | State newState = operator.apply(oldInternalState.state); |
| 246 | |
| 247 | if (newState.equals(oldInternalState.state)) { |
| 248 | return; |
| 249 | } |
| 250 | |
| 251 | // we know that we only updated the state, so the listener for the old state is the same as |
| 252 | // the listener for the new state. |
| 253 | if (oldInternalState.listener != null) { |
| 254 | long identity = Binder.clearCallingIdentity(); |
| 255 | try { |
| 256 | oldInternalState.listener.onStateChanged(oldInternalState.state, newState); |
| 257 | } finally { |
| 258 | Binder.restoreCallingIdentity(identity); |
| 259 | } |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | /** |
Soonil Nagarkar | 980ce6a | 2020-01-23 18:06:31 -0800 | [diff] [blame] | 264 | * The current allowed state of this provider. |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 265 | */ |
Soonil Nagarkar | 980ce6a | 2020-01-23 18:06:31 -0800 | [diff] [blame] | 266 | protected boolean isAllowed() { |
| 267 | return mInternalState.get().state.allowed; |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 268 | } |
| 269 | |
| 270 | /** |
| 271 | * The current provider properties of this provider. |
| 272 | */ |
| 273 | @Nullable |
| 274 | protected ProviderProperties getProperties() { |
| 275 | return mInternalState.get().state.properties; |
| 276 | } |
| 277 | |
| 278 | /** |
| 279 | * The current package set of this provider. |
| 280 | */ |
| 281 | protected Set<String> getProviderPackages() { |
| 282 | return mInternalState.get().state.providerPackageNames; |
| 283 | } |
| 284 | |
| 285 | /** |
Soonil Nagarkar | 980ce6a | 2020-01-23 18:06:31 -0800 | [diff] [blame] | 286 | * Call this method to report a change in provider allowed status. |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 287 | */ |
Soonil Nagarkar | 980ce6a | 2020-01-23 18:06:31 -0800 | [diff] [blame] | 288 | protected void setAllowed(boolean allowed) { |
| 289 | setState(state -> state.withAllowed(allowed)); |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 290 | } |
| 291 | |
| 292 | /** |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 293 | * Call this method to report a change in provider properties. |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 294 | */ |
| 295 | protected void setProperties(ProviderProperties properties) { |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 296 | setState(state -> state.withProperties(properties)); |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 297 | } |
| 298 | |
Soonil Nagarkar | d334f7b | 2019-07-08 16:16:19 -0700 | [diff] [blame] | 299 | /** |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 300 | * Call this method to report a change in provider packages. |
| 301 | */ |
| 302 | protected void setPackageNames(Set<String> packageNames) { |
| 303 | setState(state -> state.withProviderPackageNames(packageNames)); |
| 304 | } |
| 305 | |
| 306 | /** |
| 307 | * Call this method to report a new location. |
Soonil Nagarkar | d334f7b | 2019-07-08 16:16:19 -0700 | [diff] [blame] | 308 | */ |
| 309 | protected void reportLocation(Location location) { |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 310 | Listener listener = mInternalState.get().listener; |
| 311 | if (listener != null) { |
| 312 | long identity = Binder.clearCallingIdentity(); |
| 313 | try { |
| 314 | listener.onReportLocation(location); |
| 315 | } finally { |
| 316 | Binder.restoreCallingIdentity(identity); |
| 317 | } |
Soonil Nagarkar | 747cbec | 2019-09-19 14:27:03 -0700 | [diff] [blame] | 318 | } |
Soonil Nagarkar | d334f7b | 2019-07-08 16:16:19 -0700 | [diff] [blame] | 319 | } |
| 320 | |
| 321 | /** |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 322 | * Call this method to report a new location. |
Soonil Nagarkar | d334f7b | 2019-07-08 16:16:19 -0700 | [diff] [blame] | 323 | */ |
| 324 | protected void reportLocation(List<Location> locations) { |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 325 | Listener listener = mInternalState.get().listener; |
| 326 | if (listener != null) { |
| 327 | long identity = Binder.clearCallingIdentity(); |
| 328 | try { |
| 329 | listener.onReportLocation(locations); |
| 330 | } finally { |
| 331 | Binder.restoreCallingIdentity(identity); |
| 332 | } |
Soonil Nagarkar | 747cbec | 2019-09-19 14:27:03 -0700 | [diff] [blame] | 333 | } |
Soonil Nagarkar | d334f7b | 2019-07-08 16:16:19 -0700 | [diff] [blame] | 334 | } |
| 335 | |
| 336 | /** |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 337 | * Sets a new request and worksource for the provider. |
Soonil Nagarkar | d334f7b | 2019-07-08 16:16:19 -0700 | [diff] [blame] | 338 | */ |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 339 | public final void setRequest(ProviderRequest request) { |
| 340 | // all calls into the provider must be moved onto the provider thread to prevent deadlock |
| 341 | mExecutor.execute(() -> onSetRequest(request)); |
Soonil Nagarkar | 0d77ea6 | 2019-01-31 14:36:56 -0800 | [diff] [blame] | 342 | } |
| 343 | |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 344 | /** |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 345 | * Always invoked on the provider executor. |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 346 | */ |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 347 | protected abstract void onSetRequest(ProviderRequest request); |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 348 | |
| 349 | /** |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 350 | * Sends an extra command to the provider for it to interpret as it likes. |
Soonil Nagarkar | d334f7b | 2019-07-08 16:16:19 -0700 | [diff] [blame] | 351 | */ |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 352 | public final void sendExtraCommand(int uid, int pid, String command, Bundle extras) { |
| 353 | // all calls into the provider must be moved onto the provider thread to prevent deadlock |
| 354 | mExecutor.execute(() -> onExtraCommand(uid, pid, command, extras)); |
| 355 | } |
Soonil Nagarkar | d334f7b | 2019-07-08 16:16:19 -0700 | [diff] [blame] | 356 | |
| 357 | /** |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 358 | * Always invoked on the provider executor. |
| 359 | */ |
| 360 | protected void onExtraCommand(int uid, int pid, String command, Bundle extras) {} |
| 361 | |
| 362 | /** |
Soonil Nagarkar | 980ce6a | 2020-01-23 18:06:31 -0800 | [diff] [blame] | 363 | * Requests a provider to enable itself for the given user id. |
| 364 | */ |
| 365 | public final void requestSetAllowed(boolean allowed) { |
| 366 | // all calls into the provider must be moved onto the provider thread to prevent deadlock |
| 367 | mExecutor.execute(() -> onRequestSetAllowed(allowed)); |
| 368 | } |
| 369 | |
| 370 | /** |
| 371 | * Always invoked on the provider executor. |
| 372 | */ |
| 373 | protected void onRequestSetAllowed(boolean allowed) {} |
| 374 | |
| 375 | /** |
Soonil Nagarkar | 8df02f4 | 2020-01-08 13:23:26 -0800 | [diff] [blame] | 376 | * Dumps debug or log information. May be invoked from any thread. |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 377 | */ |
| 378 | public abstract void dump(FileDescriptor fd, PrintWriter pw, String[] args); |
Soonil Nagarkar | 1575a04 | 2018-10-24 17:54:54 -0700 | [diff] [blame] | 379 | } |