/* * Copyright (C) 2013-2017 microG Project Team * * 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 org.microg.gms.location; import static android.Manifest.permission.ACCESS_COARSE_LOCATION; import static android.Manifest.permission.ACCESS_FINE_LOCATION; import android.annotation.TargetApi; import android.app.AppOpsManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.location.Location; import android.os.Binder; import android.os.Build; import android.os.RemoteException; import android.util.Log; import com.google.android.gms.location.ILocationCallback; import com.google.android.gms.location.ILocationListener; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationResult; import com.google.android.gms.location.internal.LocationRequestUpdateData; import java.util.Arrays; import java.util.UUID; public class LocationRequestHelper { public static final String TAG = "GmsLocRequestHelper"; private final Context context; public final LocationRequest locationRequest; public final boolean initialHasFinePermission; public final boolean initialHasCoarsePermission; public final String packageName; public final int uid; private final boolean selfHasAppOpsRights; public ILocationListener listener; public PendingIntent pendingIntent; public ILocationCallback callback; public String id = UUID.randomUUID().toString(); private Location lastReport; private int numReports = 0; private LocationRequestHelper(Context context, LocationRequest locationRequest, String packageName, int uid) { this.context = context; this.locationRequest = locationRequest; this.packageName = packageName; this.uid = uid; this.initialHasFinePermission = context.getPackageManager().checkPermission(ACCESS_FINE_LOCATION, packageName) == PackageManager.PERMISSION_GRANTED; this.initialHasCoarsePermission = context.getPackageManager().checkPermission(ACCESS_COARSE_LOCATION, packageName) == PackageManager.PERMISSION_GRANTED; this.selfHasAppOpsRights = context.getPackageManager().checkPermission("android.permission.UPDATE_APP_OPS_STATS", context.getPackageName()) == PackageManager.PERMISSION_GRANTED; } public LocationRequestHelper(Context context, LocationRequest locationRequest, String packageName, int uid, ILocationListener listener) { this(context, locationRequest, packageName, uid); this.listener = listener; } public LocationRequestHelper(Context context, LocationRequest locationRequest, String packageName, int uid, PendingIntent pendingIntent) { this(context, locationRequest, packageName, uid); this.pendingIntent = pendingIntent; } public LocationRequestHelper(Context context, String packageName, int uid, LocationRequestUpdateData data) { this(context, data.request.request, packageName, uid); this.listener = data.listener; this.pendingIntent = data.pendingIntent; this.callback = data.callback; } public boolean isActive() { if (!hasCoarsePermission()) return false; if (listener != null) { try { return listener.asBinder().isBinderAlive(); } catch (Exception e) { return false; } } else if (pendingIntent != null) { return true; } else if (callback != null) { try { return callback.asBinder().isBinderAlive(); } catch (Exception e) { return false; } } else { return false; } } /** * @return whether to continue sending reports to this {@link LocationRequestHelper} */ public boolean report(Location location) { if (location == null) return true; if (!isActive()) return false; if (lastReport != null) { if (location.equals(lastReport)) { return true; } if (location.getTime() - lastReport.getTime() < locationRequest.getFastestInterval()) { return true; } if (location.distanceTo(lastReport) < locationRequest.getSmallestDisplacement()) { return true; } } lastReport = new Location(location); lastReport.setProvider("fused"); Log.d(TAG, "sending Location: " + location + " to " + packageName); if (listener != null) { try { listener.onLocationChanged(lastReport); } catch (RemoteException e) { return false; } } else if (pendingIntent != null) { Intent intent = new Intent(); intent.putExtra("com.google.android.location.LOCATION", lastReport); try { pendingIntent.send(context, 0, intent); } catch (PendingIntent.CanceledException e) { return false; } } else if (callback != null) { try { callback.onLocationResult(LocationResult.create(Arrays.asList(lastReport))); } catch (RemoteException e) { return false; } } numReports++; return numReports < locationRequest.getNumUpdates(); } @Override public String toString() { return "LocationRequestHelper{" + "locationRequest=" + locationRequest + ", hasFinePermission=" + hasFinePermission() + ", hasCoarsePermission=" + hasCoarsePermission() + ", packageName='" + packageName + '\'' + ", lastReport=" + lastReport + '}'; } public boolean respondsTo(ILocationListener listener) { return this.listener != null && listener != null && this.listener.asBinder().equals(listener.asBinder()); } public boolean respondsTo(ILocationCallback callback) { return this.callback != null && callback != null && this.callback.asBinder().equals(callback.asBinder()); } public boolean respondsTo(PendingIntent pendingIntent) { return this.pendingIntent != null && this.pendingIntent.equals(pendingIntent); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; LocationRequestHelper that = (LocationRequestHelper) o; if (!locationRequest.equals(that.locationRequest)) return false; if (packageName != null ? !packageName.equals(that.packageName) : that.packageName != null) return false; if (listener != null ? !listener.equals(that.listener) : that.listener != null) return false; if (pendingIntent != null ? !pendingIntent.equals(that.pendingIntent) : that.pendingIntent != null) return false; return !(callback != null ? !callback.equals(that.callback) : that.callback != null); } public boolean hasFinePermission() { if (Build.VERSION.SDK_INT >= 19) { return isAppOpsAllowed(AppOpsManager.OPSTR_FINE_LOCATION, initialHasFinePermission); } else { return initialHasFinePermission; } } public boolean hasCoarsePermission() { if (Build.VERSION.SDK_INT >= 19) { return isAppOpsAllowed(AppOpsManager.OPSTR_COARSE_LOCATION, initialHasCoarsePermission); } else { return initialHasCoarsePermission; } } @TargetApi(19) private boolean isAppOpsAllowed(String op, boolean def) { AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); if (appOpsManager == null) return def; try { if (Binder.getCallingUid() == uid && Build.VERSION.SDK_INT >= 23) { return appOpsManager.noteProxyOpNoThrow(op, packageName) == AppOpsManager.MODE_ALLOWED; } else if (Build.VERSION.SDK_INT >= 29) { return appOpsManager.noteProxyOpNoThrow(op, packageName, uid) == AppOpsManager.MODE_ALLOWED; } else if (selfHasAppOpsRights) { return appOpsManager.noteOpNoThrow(op, uid, packageName) == AppOpsManager.MODE_ALLOWED; } else { // TODO: More variant that works pre-29 and without perms? Log.w(TAG, "Can't check appops (yet)"); return def; } } catch (Exception e) { Log.w(TAG, e); return def; } } @Override public int hashCode() { int result = locationRequest.hashCode(); result = 31 * result + (packageName != null ? packageName.hashCode() : 0); result = 31 * result + (listener != null ? listener.hashCode() : 0); result = 31 * result + (pendingIntent != null ? pendingIntent.hashCode() : 0); result = 31 * result + (callback != null ? callback.hashCode() : 0); return result; } }