Initial move to CellBroadcastService dir

Bug: 135956699
Test: manually verified that framework still binds to CB service
Change-Id: I34b4e8070d56c69d1ac799159bba085e387d768a
diff --git a/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java b/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
new file mode 100644
index 0000000..abcbc20
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2013 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.cellbroadcastservice;
+
+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.Message;
+import android.provider.Telephony.CellBroadcasts;
+import android.telephony.CellInfo;
+import android.telephony.CellLocation;
+import android.telephony.SmsCbLocation;
+import android.telephony.SmsCbMessage;
+import android.telephony.TelephonyManager;
+import android.telephony.gsm.GsmCellLocation;
+import android.text.format.DateUtils;
+
+import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage;
+import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;
+import com.android.internal.telephony.CbGeoUtils.Geometry;
+
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
+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.
+ */
+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. */
+    @UnsupportedAppUsage
+    private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
+            new HashMap<>(4);
+
+    protected GsmCellBroadcastHandler(Context context) {
+        super("GsmCellBroadcastHandler", context);
+    }
+
+    @Override
+    protected void onQuitting() {
+        super.onQuitting();     // release wakelock
+    }
+
+    /**
+     * Handle a GSM cell broadcast message passed from the telephony framework.
+     * @param message
+     */
+    public void onGsmCellBroadcastSms(int slotIndex, byte[] message) {
+        sendMessage(EVENT_NEW_SMS_MESSAGE, slotIndex, -1, message);
+    }
+
+    /**
+     * Create a new CellBroadcastHandler.
+     * @param context the context to use for dispatching Intents
+     * @return the new handler
+     */
+    public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context) {
+        GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context);
+        handler.start();
+        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
+     *
+     * @return {@code True} if geo-fencing is need for some cell broadcast message.
+     */
+    private boolean handleGeoFencingTriggerMessage(
+            GeoFencingTriggerMessage geoFencingTriggerMessage, int slotIndex) {
+        final List<SmsCbMessage> cbMessages = new ArrayList<>();
+        final List<Uri> cbMessageUris = new ArrayList<>();
+
+        // Only consider the cell broadcast received within 24 hours.
+        long lastReceivedTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS;
+
+        // 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 + "=? AND "
+                + CellBroadcasts.RECEIVED_TIME + ">?";
+
+        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,
+                            Long.toString(lastReceivedTime) },
+                    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());
+                }
+            }
+        }
+
+        // ATIS doesn't specify the geo fencing maximum wait time for the cell broadcasts specified
+        // in geo fencing trigger message. We will pick the largest maximum wait time among these
+        // cell broadcasts.
+        int maximumWaitTimeSec = 0;
+        for (SmsCbMessage msg : cbMessages) {
+            maximumWaitTimeSec = Math.max(maximumWaitTimeSec, msg.getMaximumWaitingTime());
+        }
+
+        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");
+            return false;
+        }
+
+        requestLocationUpdate(location -> {
+            if (location == null) {
+                // If the location is not available, broadcast the messages directly.
+                broadcastMessage(cbMessages, cbMessageUris, slotIndex);
+            } 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), slotIndex);
+                    } else {
+                        performGeoFencing(cbMessages.get(i), cbMessageUris.get(i), broadcastArea,
+                                location, slotIndex);
+                    }
+                }
+            }
+        }, maximumWaitTimeSec);
+        return true;
+    }
+
+    /**
+     * Handle 3GPP-format Cell Broadcast messages sent from radio.
+     *
+     * @param message the message to process
+     * @return true if need to wait for geo-fencing or an ordered broadcast was sent.
+     */
+    @Override
+    protected boolean handleSmsMessage(Message message) {
+        // For GSM, message.obj should be a byte[]
+        int slotIndex = message.arg1;
+        if (message.obj instanceof byte[]) {
+            byte[] pdu = (byte[]) message.obj;
+            SmsCbHeader header = createSmsCbHeader(pdu);
+            if (header == null) return false;
+
+            if (header.getServiceCategory() == MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) {
+                GeoFencingTriggerMessage triggerMessage =
+                        GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu);
+                if (triggerMessage != null) {
+                    return handleGeoFencingTriggerMessage(triggerMessage, slotIndex);
+                }
+            } else {
+                SmsCbMessage cbMessage = handleGsmBroadcastSms(header, pdu, slotIndex);
+                if (cbMessage != null) {
+                    handleBroadcastSms(cbMessage);
+                    return true;
+                }
+                if (VDBG) log("Not handled GSM broadcasts.");
+            }
+        }
+        return super.handleSmsMessage(message);
+    }
+
+    // return the cell location from the first returned cell info, prioritizing GSM
+    private CellLocation getCellLocation() {
+        TelephonyManager tm =
+                (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+        List<CellInfo> infos = tm.getAllCellInfo();
+        for (CellInfo info : infos) {
+            CellLocation cl = info.getCellIdentity().asCellLocation();
+            if (cl instanceof GsmCellLocation) {
+                return cl;
+            }
+        }
+        // If no GSM, return first in list
+        if (infos != null && !infos.isEmpty() && infos.get(0) != null) {
+            return infos.get(0).getCellIdentity().asCellLocation();
+        }
+        return CellLocation.getEmpty();
+    }
+
+
+    /**
+     * Handle 3GPP format SMS-CB message.
+     * @param header the cellbroadcast header.
+     * @param receivedPdu the received PDUs as a byte[]
+     */
+    private SmsCbMessage handleGsmBroadcastSms(SmsCbHeader header, byte[] receivedPdu,
+            int slotIndex) {
+        try {
+            if (VDBG) {
+                int pduLength = receivedPdu.length;
+                for (int i = 0; i < pduLength; i += 8) {
+                    StringBuilder sb = new StringBuilder("SMS CB pdu data: ");
+                    for (int j = i; j < i + 8 && j < pduLength; j++) {
+                        int b = receivedPdu[j] & 0xff;
+                        if (b < 0x10) {
+                            sb.append('0');
+                        }
+                        sb.append(Integer.toHexString(b)).append(' ');
+                    }
+                    log(sb.toString());
+                }
+            }
+
+            if (VDBG) log("header=" + header);
+            TelephonyManager tm =
+                    (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+            // TODO make a systemAPI for getNetworkOperatorForSlotIndex
+            String plmn = tm.getNetworkOperatorForPhone(slotIndex);
+            int lac = -1;
+            int cid = -1;
+            CellLocation cl = getCellLocation();
+            // Check if cell location is GsmCellLocation.  This is required to support
+            // dual-mode devices such as CDMA/LTE devices that require support for
+            // both 3GPP and 3GPP2 format messages
+            if (cl instanceof GsmCellLocation) {
+                GsmCellLocation cellLocation = (GsmCellLocation) cl;
+                lac = cellLocation.getLac();
+                cid = cellLocation.getCid();
+            }
+
+            SmsCbLocation location;
+            switch (header.getGeographicalScope()) {
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE:
+                    location = new SmsCbLocation(plmn, lac, -1);
+                    break;
+
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
+                    location = new SmsCbLocation(plmn, lac, cid);
+                    break;
+
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
+                default:
+                    location = new SmsCbLocation(plmn);
+                    break;
+            }
+
+            byte[][] pdus;
+            int pageCount = header.getNumberOfPages();
+            if (pageCount > 1) {
+                // Multi-page message
+                SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location);
+
+                // Try to find other pages of the same message
+                pdus = mSmsCbPageMap.get(concatInfo);
+
+                if (pdus == null) {
+                    // This is the first page of this message, make room for all
+                    // pages and keep until complete
+                    pdus = new byte[pageCount][];
+
+                    mSmsCbPageMap.put(concatInfo, pdus);
+                }
+
+                if (VDBG) log("pdus size=" + pdus.length);
+                // Page parameter is one-based
+                pdus[header.getPageIndex() - 1] = receivedPdu;
+
+                for (byte[] pdu : pdus) {
+                    if (pdu == null) {
+                        // Still missing pages, exit
+                        log("still missing pdu");
+                        return null;
+                    }
+                }
+
+                // Message complete, remove and dispatch
+                mSmsCbPageMap.remove(concatInfo);
+            } else {
+                // Single page message
+                pdus = new byte[1][];
+                pdus[0] = receivedPdu;
+            }
+
+            // Remove messages that are out of scope to prevent the map from
+            // growing indefinitely, containing incomplete messages that were
+            // never assembled
+            Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator();
+
+            while (iter.hasNext()) {
+                SmsCbConcatInfo info = iter.next();
+
+                if (!info.matchesLocation(plmn, lac, cid)) {
+                    iter.remove();
+                }
+            }
+
+            return GsmSmsCbMessage.createSmsCbMessage(mContext, header, location, pdus, slotIndex);
+
+        } catch (RuntimeException e) {
+            loge("Error in decoding SMS CB pdu", e);
+            return null;
+        }
+    }
+
+    private SmsCbHeader createSmsCbHeader(byte[] bytes) {
+        try {
+            return new SmsCbHeader(bytes);
+        } 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.
+     */
+    private static final class SmsCbConcatInfo {
+
+        private final SmsCbHeader mHeader;
+        private final SmsCbLocation mLocation;
+
+        @UnsupportedAppUsage
+        SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) {
+            mHeader = header;
+            mLocation = location;
+        }
+
+        @Override
+        public int hashCode() {
+            return (mHeader.getSerialNumber() * 31) + mLocation.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof SmsCbConcatInfo) {
+                SmsCbConcatInfo other = (SmsCbConcatInfo) obj;
+
+                // Two pages match if they have the same serial number (which includes the
+                // geographical scope and update number), and both pages belong to the same
+                // location (PLMN, plus LAC and CID if these are part of the geographical scope).
+                return mHeader.getSerialNumber() == other.mHeader.getSerialNumber()
+                        && mLocation.equals(other.mLocation);
+            }
+
+            return false;
+        }
+
+        /**
+         * Compare the location code for this message to the current location code. The match is
+         * relative to the geographical scope of the message, which determines whether the LAC
+         * and Cell ID are saved in mLocation or set to -1 to match all values.
+         *
+         * @param plmn the current PLMN
+         * @param lac the current Location Area (GSM) or Service Area (UMTS)
+         * @param cid the current Cell ID
+         * @return true if this message is valid for the current location; false otherwise
+         */
+        @UnsupportedAppUsage
+        public boolean matchesLocation(String plmn, int lac, int cid) {
+            return mLocation.isInLocationArea(plmn, lac, cid);
+        }
+    }
+}