Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 82e27be1 authored by Pengquan Meng's avatar Pengquan Meng Committed by android-build-merger
Browse files

Merge "Add geo-fencing for cellbroadcast" am: da49c2c1

am: c0fd8ff8

Change-Id: I5b9beff0ad529bbde846a14cf226000f9f8176ba
parents 7f6c7881 c0fd8ff8
Loading
Loading
Loading
Loading
+263 −4
Original line number Diff line number Diff line
@@ -16,36 +16,59 @@

package com.android.internal.telephony;

import static android.content.PermissionChecker.PERMISSION_GRANTED;
import static android.provider.Settings.Secure.CMAS_ADDITIONAL_BROADCAST_PKG;

import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.AppOpsManager;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.PermissionChecker;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.UserHandle;
import android.provider.Settings;
import android.provider.Telephony;
import android.provider.Telephony.CellBroadcasts;
import android.telephony.SmsCbMessage;
import android.telephony.SubscriptionManager;
import android.util.LocalLog;
import android.util.Log;

import com.android.internal.telephony.CbGeoUtils.Geometry;
import com.android.internal.telephony.CbGeoUtils.LatLng;
import com.android.internal.telephony.metrics.TelephonyMetrics;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Dispatch new Cell Broadcasts to receivers. Acquires a private wakelock until the broadcast
 * completes and our result receiver is called.
 */
public class CellBroadcastHandler extends WakeLockStateMachine {
    private static final String EXTRA_MESSAGE = "message";

    private final LocalLog mLocalLog = new LocalLog(100);

    private static final String EXTRA_MESSAGE = "message";
    protected static final Uri CELL_BROADCAST_URI = Uri.parse("content://cellbroadcasts_fwk");

    /** Uses to request the location update. */
    public final LocationRequester mLocationRequester;

    private CellBroadcastHandler(Context context, Phone phone) {
        this("CellBroadcastHandler", context, phone);
@@ -53,6 +76,10 @@ public class CellBroadcastHandler extends WakeLockStateMachine {

    protected CellBroadcastHandler(String debugTag, Context context, Phone phone) {
        super(debugTag, context, phone);
        mLocationRequester = new LocationRequester(
                context,
                (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE),
                getHandler().getLooper());
    }

    /**
@@ -89,9 +116,6 @@ public class CellBroadcastHandler extends WakeLockStateMachine {
     * @param message the Cell Broadcast to broadcast
     */
    protected void handleBroadcastSms(SmsCbMessage message) {
        String receiverPermission;
        int appOp;

        // Log Cellbroadcast msg received event
        TelephonyMetrics metrics = TelephonyMetrics.getInstance();
        metrics.writeNewCBSms(mPhone.getPhoneId(), message.getMessageFormat(),
@@ -99,6 +123,92 @@ public class CellBroadcastHandler extends WakeLockStateMachine {
                message.getServiceCategory(), message.getSerialNumber(),
                System.currentTimeMillis());

        // TODO: Database inserting can be time consuming, therefore this should be changed to
        // asynchronous.
        ContentValues cv = message.getContentValues();
        Uri uri = mContext.getContentResolver().insert(CELL_BROADCAST_URI, cv);

        if (message.needGeoFencingCheck()) {
            if (DBG) {
                log("Request location update for geo-fencing. serialNumber = "
                        + message.getSerialNumber());
            }

            requestLocationUpdate(location -> {
                if (location == null) {
                    // Broadcast the message directly if the location is not available.
                    broadcastMessage(message, uri);
                } else {
                    performGeoFencing(message, uri, message.getGeometries(), location);
                }
            });
        } else {
            if (DBG) {
                log("Broadcast the message directly because no geo-fencing required, "
                        + "serialNumber = " + message.getSerialNumber()
                        + " needGeoFencing = " + message.needGeoFencingCheck());
            }
            broadcastMessage(message, uri);
        }
    }

    /**
     * Perform a geo-fencing check for {@code message}. Broadcast the {@code message} if the
     * {@code location} is inside the {@code broadcastArea}.
     * @param message the message need to geo-fencing check
     * @param uri the message's uri
     * @param broadcastArea the broadcast area of the message
     * @param location current location
     */
    protected void performGeoFencing(SmsCbMessage message, Uri uri, List<Geometry> broadcastArea,
            LatLng location) {

        if (DBG) {
            logd("Perform geo-fencing check for message identifier = "
                    + message.getServiceCategory()
                    + " serialNumber = " + message.getSerialNumber());
        }

        for (Geometry geo : broadcastArea) {
            if (geo.contains(location)) {
                broadcastMessage(message, uri);
                return;
            }
        }

        if (DBG) {
            logd("Device location is outside the broadcast area "
                    + CbGeoUtils.encodeGeometriesToString(broadcastArea));
        }
    }

    /**
     * Request a single location update.
     * @param callback a callback will be called when the location is available.
     */
    protected void requestLocationUpdate(LocationUpdateCallback callback) {
        mLocationRequester.requestLocationUpdate(callback);
    }

    /**
     * Broadcast a list of cell broadcast messages.
     * @param cbMessages a list of cell broadcast message.
     * @param cbMessageUris the corresponding {@link Uri} of the cell broadcast messages.
     */
    protected void broadcastMessage(List<SmsCbMessage> cbMessages, List<Uri> cbMessageUris) {
        for (int i = 0; i < cbMessages.size(); i++) {
            broadcastMessage(cbMessages.get(i), cbMessageUris.get(i));
        }
    }

    /**
     * Broadcast the {@code message} to the applications.
     * @param message a message need to broadcast
     * @param messageUri message's uri
     */
    protected void broadcastMessage(@NonNull SmsCbMessage message, @Nullable Uri messageUri) {
        String receiverPermission;
        int appOp;
        String msg;
        Intent intent;
        if (message.isEmergencyMessage()) {
@@ -155,6 +265,13 @@ public class CellBroadcastHandler extends WakeLockStateMachine {
            mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, receiverPermission, appOp,
                    mReceiver, getHandler(), Activity.RESULT_OK, null, null);
        }

        if (messageUri != null) {
            ContentValues cv = new ContentValues();
            cv.put(CellBroadcasts.MESSAGE_BROADCASTED, 1);
            mContext.getContentResolver().update(CELL_BROADCAST_URI, cv,
                    CellBroadcasts._ID + "=?", new String[] {messageUri.getLastPathSegment()});
        }
    }

    @Override
@@ -163,4 +280,146 @@ public class CellBroadcastHandler extends WakeLockStateMachine {
        mLocalLog.dump(fd, pw, args);
        pw.flush();
    }

    /** The callback interface of a location request. */
    public interface LocationUpdateCallback {
        /**
         * Call when the location update is available.
         * @param location a location in (latitude, longitude) format, or {@code null} if the
         * location service is not available.
         */
        void onLocationUpdate(@Nullable LatLng location);
    }

    private static final class LocationRequester {
        private static final String TAG = LocationRequester.class.getSimpleName();

        /**
         * Trigger this event when the {@link LocationManager} is not responded within the given
         * time.
         */
        private static final int EVENT_LOCATION_REQUEST_TIMEOUT = 1;

        /** Request a single location update. */
        private static final int EVENT_REQUEST_LOCATION_UPDATE = 2;

        /** Default expired time of the location request. */
        private static final int LOCATION_REQUEST_TIMEOUT_MILLIS = 30 * 1000;

        /**
         * Request location update from network or gps location provider. Network provider will be
         * used if available, otherwise use the gps provider.
         */
        private static final List<String> LOCATION_PROVIDERS = Arrays.asList(
                LocationManager.NETWORK_PROVIDER, LocationManager.GPS_PROVIDER);

        private final LocationManager mLocationManager;
        private final Looper mLooper;
        private final List<LocationUpdateCallback> mCallbacks;
        private final Context mContext;
        private Handler mLocationHandler;

        LocationRequester(Context context, LocationManager locationManager, Looper looper) {
            mLocationManager = locationManager;
            mLooper = looper;
            mCallbacks = new ArrayList<>();
            mContext = context;
            mLocationHandler = new LocationHandler(looper);
        }

        /**
         * Request a single location update. If the location is not available, a callback with
         * {@code null} location will be called immediately.
         * @param callback a callback to the the response when the location is available
         */
        void requestLocationUpdate(@NonNull LocationUpdateCallback callback) {
            mLocationHandler.obtainMessage(EVENT_REQUEST_LOCATION_UPDATE, callback).sendToTarget();
        }

        private void onLocationUpdate(@Nullable LatLng location) {
            for (LocationUpdateCallback callback : mCallbacks) {
                callback.onLocationUpdate(location);
            }
            mCallbacks.clear();
        }

        private void requestLocationUpdateInternal(@NonNull LocationUpdateCallback callback) {
            if (DBG) Log.d(TAG, "requestLocationUpdate");
            if (!isLocationServiceAvailable()) {
                if (DBG) {
                    Log.d(TAG, "Can't request location update because of no location permission");
                }
                callback.onLocationUpdate(null);
                return;
            }

            // TODO: handle the "Geo-fencing Maximum Wait" defined in ATIS-0700041 Section 5.2.3
            if (!mLocationHandler.hasMessages(EVENT_LOCATION_REQUEST_TIMEOUT)) {
                mLocationHandler.sendMessageDelayed(
                        mLocationHandler.obtainMessage(EVENT_LOCATION_REQUEST_TIMEOUT),
                        LOCATION_REQUEST_TIMEOUT_MILLIS);
            }

            mCallbacks.add(callback);

            for (String provider : LOCATION_PROVIDERS) {
                if (mLocationManager.isProviderEnabled(provider)) {
                    mLocationManager.requestSingleUpdate(provider, mLocationListener, mLooper);
                    break;
                }
            }
        }

        private boolean isLocationServiceAvailable() {
            if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)
                    && !hasPermission(Manifest.permission.ACCESS_COARSE_LOCATION)) return false;
            for (String provider : LOCATION_PROVIDERS) {
                if (mLocationManager.isProviderEnabled(provider)) return true;
            }
            return false;
        }

        private boolean hasPermission(String permission) {
            return PermissionChecker.checkCallingOrSelfPermission(mContext, permission)
                    == PERMISSION_GRANTED;
        }

        private final LocationListener mLocationListener = new LocationListener() {
            @Override
            public void onLocationChanged(Location location) {
                mLocationHandler.removeMessages(EVENT_LOCATION_REQUEST_TIMEOUT);
                onLocationUpdate(new LatLng(location.getLatitude(), location.getLongitude()));
            }

            @Override
            public void onStatusChanged(String provider, int status, Bundle extras) {}

            @Override
            public void onProviderEnabled(String provider) {}

            @Override
            public void onProviderDisabled(String provider) {}
        };

        private final class LocationHandler extends Handler {
            LocationHandler(Looper looper) {
                super(looper);
            }

            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case EVENT_LOCATION_REQUEST_TIMEOUT:
                        if (DBG) Log.d(TAG, "location request timeout");
                        onLocationUpdate(null);
                        break;
                    case EVENT_REQUEST_LOCATION_UPDATE:
                        requestLocationUpdateInternal((LocationUpdateCallback) msg.obj);
                        break;
                    default:
                        Log.e(TAG, "Unsupported message type " + msg.what);
                }
            }
        }
    }
}
+15 −16
Original line number Diff line number Diff line
@@ -38,7 +38,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 * {@link #quit}.
 */
public abstract class WakeLockStateMachine extends StateMachine {
    protected static final boolean DBG = true;    // TODO: change to false
    protected static final boolean DBG = Build.IS_DEBUGGABLE;

    private final PowerManager.WakeLock mWakeLock;

@@ -75,7 +75,8 @@ public abstract class WakeLockStateMachine extends StateMachine {

        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, debugTag);
        mWakeLock.acquire();    // wake lock released after we enter idle state
        // wake lock released after we enter idle state
        mWakeLock.acquire();

        addState(mDefaultState);
        addState(mIdleState, mDefaultState);
@@ -83,6 +84,16 @@ public abstract class WakeLockStateMachine extends StateMachine {
        setInitialState(mIdleState);
    }

    private void releaseWakeLock() {
        if (mWakeLock.isHeld()) {
            mWakeLock.release();
        }

        if (mWakeLock.isHeld()) {
            loge("Wait lock is held after release.");
        }
    }

    /**
     * Tell the state machine to quit after processing all messages.
     */
@@ -155,15 +166,7 @@ public abstract class WakeLockStateMachine extends StateMachine {
                    return HANDLED;

                case EVENT_RELEASE_WAKE_LOCK:
                    mWakeLock.release();
                    if (DBG) {
                        if (mWakeLock.isHeld()) {
                            // this is okay as long as we call release() for every acquire()
                            log("mWakeLock is still held after release");
                        } else {
                            log("mWakeLock released");
                        }
                    }
                    releaseWakeLock();
                    return HANDLED;

                default:
@@ -191,11 +194,7 @@ public abstract class WakeLockStateMachine extends StateMachine {
                    return HANDLED;

                case EVENT_RELEASE_WAKE_LOCK:
                    mWakeLock.release();    // decrement wakelock from previous entry to Idle
                    if (!mWakeLock.isHeld()) {
                        // wakelock should still be held until 3 seconds after we enter Idle
                        loge("mWakeLock released while still in WaitingState!");
                    }
                    releaseWakeLock();
                    return HANDLED;

                default:
+121 −7
Original line number Diff line number Diff line
@@ -16,20 +16,32 @@

package com.android.internal.telephony.gsm;

import static com.android.internal.telephony.gsm.SmsCbConstants.MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER;

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncResult;
import android.os.Message;
import android.provider.Telephony.CellBroadcasts;
import android.telephony.CellLocation;
import android.telephony.SmsCbLocation;
import android.telephony.SmsCbMessage;
import android.telephony.TelephonyManager;
import android.telephony.gsm.GsmCellLocation;

import com.android.internal.telephony.CbGeoUtils.Geometry;
import com.android.internal.telephony.CellBroadcastHandler;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.gsm.GsmSmsCbMessage.GeoFencingTriggerMessage;
import com.android.internal.telephony.gsm.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

/**
 * Handler for 3GPP format Cell Broadcasts. Parent class can also handle CDMA Cell Broadcasts.
@@ -37,6 +49,9 @@ import java.util.Iterator;
public class GsmCellBroadcastHandler extends CellBroadcastHandler {
    private static final boolean VDBG = false;  // log CB PDU data

    /** Indicates that a message is not being broadcasted. */
    private static final String MESSAGE_NOT_BROADCASTED = "0";

    /** This map holds incomplete concatenated messages waiting for assembly. */
    private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
            new HashMap<SmsCbConcatInfo, byte[][]>(4);
@@ -64,6 +79,82 @@ public class GsmCellBroadcastHandler extends CellBroadcastHandler {
        return handler;
    }

    /**
     * Find the cell broadcast messages specify by the geo-fencing trigger message and perform a
     * geo-fencing check for these messages.
     * @param geoFencingTriggerMessage the trigger message
     */
    private void handleGeoFencingTriggerMessage(GeoFencingTriggerMessage geoFencingTriggerMessage) {
        final List<SmsCbMessage> cbMessages = new ArrayList<>();
        final List<Uri> cbMessageUris = new ArrayList<>();

        // Find the cell broadcast message identify by the message identifier and serial number
        // and is not broadcasted.
        String where = CellBroadcasts.SERVICE_CATEGORY + "=? AND "
                + CellBroadcasts.SERIAL_NUMBER + "=? AND "
                + CellBroadcasts.MESSAGE_BROADCASTED + "=?";

        ContentResolver resolver = mContext.getContentResolver();
        for (CellBroadcastIdentity identity : geoFencingTriggerMessage.cbIdentifiers) {
            try (Cursor cursor = resolver.query(CELL_BROADCAST_URI,
                    CellBroadcasts.QUERY_COLUMNS_FWK,
                    where,
                    new String[] { Integer.toString(identity.messageIdentifier),
                            Integer.toString(identity.serialNumber), MESSAGE_NOT_BROADCASTED},
                    null /* sortOrder */)) {
                if (cursor != null) {
                    while (cursor.moveToNext()) {
                        cbMessages.add(SmsCbMessage.createFromCursor(cursor));
                        cbMessageUris.add(ContentUris.withAppendedId(CELL_BROADCAST_URI,
                                cursor.getInt(cursor.getColumnIndex(CellBroadcasts._ID))));
                    }
                }
            }
        }

        List<Geometry> commonBroadcastArea = new ArrayList<>();
        if (geoFencingTriggerMessage.shouldShareBroadcastArea()) {
            for (SmsCbMessage msg : cbMessages) {
                if (msg.getGeometries() != null) {
                    commonBroadcastArea.addAll(msg.getGeometries());
                }
            }
        }

        if (DBG) {
            logd("Geo-fencing trigger message = " + geoFencingTriggerMessage);
            for (SmsCbMessage msg : cbMessages) {
                logd(msg.toString());
            }
        }

        if (cbMessages.isEmpty()) {
            if (DBG) logd("No CellBroadcast message need to be broadcasted");

            // Need to send this event to make the state machine back to Idle.
            if (mReceiverCount.get() == 0) sendMessage(EVENT_BROADCAST_COMPLETE);
            return;
        }

        requestLocationUpdate(location -> {
            if (location == null) {
                // If the location is not available, broadcast the messages directly.
                broadcastMessage(cbMessages, cbMessageUris);
            } else {
                for (int i = 0; i < cbMessages.size(); i++) {
                    List<Geometry> broadcastArea = commonBroadcastArea.isEmpty()
                            ? commonBroadcastArea : cbMessages.get(i).getGeometries();
                    if (broadcastArea == null || broadcastArea.isEmpty()) {
                        broadcastMessage(cbMessages.get(i), cbMessageUris.get(i));
                    } else {
                        performGeoFencing(cbMessages.get(i), cbMessageUris.get(i), broadcastArea,
                                location);
                    }
                }
            }
        });
    }

    /**
     * Handle 3GPP-format Cell Broadcast messages sent from radio.
     *
@@ -73,21 +164,36 @@ public class GsmCellBroadcastHandler extends CellBroadcastHandler {
    @Override
    protected boolean handleSmsMessage(Message message) {
        if (message.obj instanceof AsyncResult) {
            SmsCbMessage cbMessage = handleGsmBroadcastSms((AsyncResult) message.obj);
            SmsCbHeader header = createSmsCbHeader((AsyncResult) message.obj);
            if (header == null) return false;

            AsyncResult ar = (AsyncResult) message.obj;
            byte[] pdu = (byte[]) ar.result;
            if (header.getServiceCategory() == MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) {
                GeoFencingTriggerMessage triggerMessage =
                        GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu);
                if (triggerMessage != null) {
                    handleGeoFencingTriggerMessage(triggerMessage);
                    return true;
                }
            } else {
                SmsCbMessage cbMessage = handleGsmBroadcastSms(header, ar);
                if (cbMessage != null) {
                    handleBroadcastSms(cbMessage);
                    return true;
                }
                if (VDBG) log("Not handled GSM broadcasts.");
            }
        }
        return super.handleSmsMessage(message);
    }

    /**
     * Handle 3GPP format SMS-CB message.
     * @param header the cellbroadcast header.
     * @param ar the AsyncResult containing the received PDUs
     */
    private SmsCbMessage handleGsmBroadcastSms(AsyncResult ar) {
    private SmsCbMessage handleGsmBroadcastSms(SmsCbHeader header, AsyncResult ar) {
        try {
            byte[] receivedPdu = (byte[]) ar.result;

@@ -106,7 +212,6 @@ public class GsmCellBroadcastHandler extends CellBroadcastHandler {
                }
            }

            SmsCbHeader header = new SmsCbHeader(receivedPdu);
            if (VDBG) log("header=" + header);
            String plmn = TelephonyManager.from(mContext).getNetworkOperatorForPhone(
                    mPhone.getPhoneId());
@@ -197,6 +302,15 @@ public class GsmCellBroadcastHandler extends CellBroadcastHandler {
        }
    }

    private SmsCbHeader createSmsCbHeader(AsyncResult ar) {
        try {
            return new SmsCbHeader((byte[]) ar.result);
        } catch (Exception ex) {
            loge("Can't create SmsCbHeader, ex = " + ex.toString());
            return null;
        }
    }

    /**
     * Holds all info about a message page needed to assemble a complete concatenated message.
     */