1/*
2 * Copyright (C) 2015 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
17package com.android.messaging.sms;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.database.Cursor;
22import android.database.sqlite.SQLiteDatabase;
23import android.database.sqlite.SQLiteException;
24import android.net.Uri;
25import android.provider.Telephony;
26import android.support.v7.mms.ApnSettingsLoader;
27import android.support.v7.mms.MmsManager;
28import android.text.TextUtils;
29import android.util.SparseArray;
30
31import com.android.messaging.datamodel.data.ParticipantData;
32import com.android.messaging.mmslib.SqliteWrapper;
33import com.android.messaging.util.BugleGservices;
34import com.android.messaging.util.BugleGservicesKeys;
35import com.android.messaging.util.LogUtil;
36import com.android.messaging.util.OsUtil;
37import com.android.messaging.util.PhoneUtils;
38
39import java.net.URI;
40import java.net.URISyntaxException;
41import java.util.ArrayList;
42import java.util.List;
43
44/**
45 * APN loader for default SMS SIM
46 *
47 * This loader tries to load APNs from 3 sources in order:
48 * 1. Gservices setting
49 * 2. System APN table
50 * 3. Local APN table
51 */
52public class BugleApnSettingsLoader implements ApnSettingsLoader {
53    /**
54     * The base implementation of an APN
55     */
56    private static class BaseApn implements Apn {
57        /**
58         * Create a base APN from parameters
59         *
60         * @param typesIn the APN type field
61         * @param mmscIn the APN mmsc field
62         * @param proxyIn the APN mmsproxy field
63         * @param portIn the APN mmsport field
64         * @return an instance of base APN, or null if any of the parameter is invalid
65         */
66        public static BaseApn from(final String typesIn, final String mmscIn, final String proxyIn,
67                final String portIn) {
68            if (!isValidApnType(trimWithNullCheck(typesIn), APN_TYPE_MMS)) {
69                return null;
70            }
71            String mmsc = trimWithNullCheck(mmscIn);
72            if (TextUtils.isEmpty(mmsc)) {
73                return null;
74            }
75            mmsc = trimV4AddrZeros(mmsc);
76            try {
77                new URI(mmsc);
78            } catch (final URISyntaxException e) {
79                return null;
80            }
81            String mmsProxy = trimWithNullCheck(proxyIn);
82            int mmsProxyPort = 80;
83            if (!TextUtils.isEmpty(mmsProxy)) {
84                mmsProxy = trimV4AddrZeros(mmsProxy);
85                final String portString = trimWithNullCheck(portIn);
86                if (portString != null) {
87                    try {
88                        mmsProxyPort = Integer.parseInt(portString);
89                    } catch (final NumberFormatException e) {
90                        // Ignore, just use 80 to try
91                    }
92                }
93            }
94            return new BaseApn(mmsc, mmsProxy, mmsProxyPort);
95        }
96
97        private final String mMmsc;
98        private final String mMmsProxy;
99        private final int mMmsProxyPort;
100
101        public BaseApn(final String mmsc, final String proxy, final int port) {
102            mMmsc = mmsc;
103            mMmsProxy = proxy;
104            mMmsProxyPort = port;
105        }
106
107        @Override
108        public String getMmsc() {
109            return mMmsc;
110        }
111
112        @Override
113        public String getMmsProxy() {
114            return mMmsProxy;
115        }
116
117        @Override
118        public int getMmsProxyPort() {
119            return mMmsProxyPort;
120        }
121
122        @Override
123        public void setSuccess() {
124            // Do nothing
125        }
126
127        public boolean equals(final BaseApn other) {
128            return TextUtils.equals(mMmsc, other.getMmsc()) &&
129                    TextUtils.equals(mMmsProxy, other.getMmsProxy()) &&
130                    mMmsProxyPort == other.getMmsProxyPort();
131        }
132    }
133
134    /**
135     * The APN represented by the local APN table row
136     */
137    private static class DatabaseApn implements Apn {
138        private static final ContentValues CURRENT_NULL_VALUE;
139        private static final ContentValues CURRENT_SET_VALUE;
140        static {
141            CURRENT_NULL_VALUE = new ContentValues(1);
142            CURRENT_NULL_VALUE.putNull(Telephony.Carriers.CURRENT);
143            CURRENT_SET_VALUE = new ContentValues(1);
144            CURRENT_SET_VALUE.put(Telephony.Carriers.CURRENT, "1"); // 1 for auto selected APN
145        }
146        private static final String CLEAR_UPDATE_SELECTION = Telephony.Carriers.CURRENT + " =?";
147        private static final String[] CLEAR_UPDATE_SELECTION_ARGS = new String[] { "1" };
148        private static final String SET_UPDATE_SELECTION = Telephony.Carriers._ID + " =?";
149
150        /**
151         * Create an APN loaded from local database
152         *
153         * @param apns the in-memory APN list
154         * @param typesIn the APN type field
155         * @param mmscIn the APN mmsc field
156         * @param proxyIn the APN mmsproxy field
157         * @param portIn the APN mmsport field
158         * @param rowId the APN's row ID in database
159         * @param current the value of CURRENT column in database
160         * @return an in-memory APN instance for database APN row, null if parameter invalid
161         */
162        public static DatabaseApn from(final List<Apn> apns, final String typesIn,
163                final String mmscIn, final String proxyIn, final String portIn,
164                final long rowId, final int current) {
165            if (apns == null) {
166                return null;
167            }
168            final BaseApn base = BaseApn.from(typesIn, mmscIn, proxyIn, portIn);
169            if (base == null) {
170                return null;
171            }
172            for (final ApnSettingsLoader.Apn apn : apns) {
173                if (apn instanceof DatabaseApn && ((DatabaseApn) apn).equals(base)) {
174                    return null;
175                }
176            }
177            return new DatabaseApn(apns, base, rowId, current);
178        }
179
180        private final List<Apn> mApns;
181        private final BaseApn mBase;
182        private final long mRowId;
183        private int mCurrent;
184
185        public DatabaseApn(final List<Apn> apns, final BaseApn base, final long rowId,
186                final int current) {
187            mApns = apns;
188            mBase = base;
189            mRowId = rowId;
190            mCurrent = current;
191        }
192
193        @Override
194        public String getMmsc() {
195            return mBase.getMmsc();
196        }
197
198        @Override
199        public String getMmsProxy() {
200            return mBase.getMmsProxy();
201        }
202
203        @Override
204        public int getMmsProxyPort() {
205            return mBase.getMmsProxyPort();
206        }
207
208        @Override
209        public void setSuccess() {
210            moveToListHead();
211            setCurrentInDatabase();
212        }
213
214        /**
215         * Try to move this APN to the head of in-memory list
216         */
217        private void moveToListHead() {
218            // If this is being marked as a successful APN, move it to the top of the list so
219            // next time it will be tried first
220            boolean moved = false;
221            synchronized (mApns) {
222                if (mApns.get(0) != this) {
223                    mApns.remove(this);
224                    mApns.add(0, this);
225                    moved = true;
226                }
227            }
228            if (moved) {
229                LogUtil.d(LogUtil.BUGLE_TAG, "Set APN ["
230                        + "MMSC=" + getMmsc() + ", "
231                        + "PROXY=" + getMmsProxy() + ", "
232                        + "PORT=" + getMmsProxyPort() + "] to be first");
233            }
234        }
235
236        /**
237         * Try to set the APN to be CURRENT in its database table
238         */
239        private void setCurrentInDatabase() {
240            synchronized (this) {
241                if (mCurrent > 0) {
242                    // Already current
243                    return;
244                }
245                mCurrent = 1;
246            }
247            LogUtil.d(LogUtil.BUGLE_TAG, "Set APN @" + mRowId + " to be CURRENT in local db");
248            final SQLiteDatabase database = ApnDatabase.getApnDatabase().getWritableDatabase();
249            database.beginTransaction();
250            try {
251                // clear the previous current=1 apn
252                // we don't clear current=2 apn since it is manually selected by user
253                // and we should not override it.
254                database.update(ApnDatabase.APN_TABLE, CURRENT_NULL_VALUE,
255                        CLEAR_UPDATE_SELECTION, CLEAR_UPDATE_SELECTION_ARGS);
256                // set this one to be current (1)
257                database.update(ApnDatabase.APN_TABLE, CURRENT_SET_VALUE, SET_UPDATE_SELECTION,
258                        new String[] { Long.toString(mRowId) });
259                database.setTransactionSuccessful();
260            } finally {
261                database.endTransaction();
262            }
263        }
264
265        public boolean equals(final BaseApn other) {
266            if (other == null) {
267                return false;
268            }
269            return mBase.equals(other);
270        }
271    }
272
273    /**
274     * APN_TYPE_ALL is a special type to indicate that this APN entry can
275     * service all data connections.
276     */
277    public static final String APN_TYPE_ALL = "*";
278    /** APN type for MMS traffic */
279    public static final String APN_TYPE_MMS = "mms";
280
281    private static final String[] APN_PROJECTION_SYSTEM = {
282            Telephony.Carriers.TYPE,
283            Telephony.Carriers.MMSC,
284            Telephony.Carriers.MMSPROXY,
285            Telephony.Carriers.MMSPORT,
286    };
287    private static final String[] APN_PROJECTION_LOCAL = {
288            Telephony.Carriers.TYPE,
289            Telephony.Carriers.MMSC,
290            Telephony.Carriers.MMSPROXY,
291            Telephony.Carriers.MMSPORT,
292            Telephony.Carriers.CURRENT,
293            Telephony.Carriers._ID,
294    };
295    private static final int COLUMN_TYPE         = 0;
296    private static final int COLUMN_MMSC         = 1;
297    private static final int COLUMN_MMSPROXY     = 2;
298    private static final int COLUMN_MMSPORT      = 3;
299    private static final int COLUMN_CURRENT      = 4;
300    private static final int COLUMN_ID           = 5;
301
302    private static final String SELECTION_APN = Telephony.Carriers.APN + "=?";
303    private static final String SELECTION_CURRENT = Telephony.Carriers.CURRENT + " IS NOT NULL";
304    private static final String SELECTION_NUMERIC = Telephony.Carriers.NUMERIC + "=?";
305    private static final String ORDER_BY = Telephony.Carriers.CURRENT + " DESC";
306
307    private final Context mContext;
308
309    // Cached APNs for subIds
310    private final SparseArray<List<ApnSettingsLoader.Apn>> mApnsCache;
311
312    public BugleApnSettingsLoader(final Context context) {
313        mContext = context;
314        mApnsCache = new SparseArray<>();
315    }
316
317    @Override
318    public List<ApnSettingsLoader.Apn> get(final String apnName) {
319        final int subId = PhoneUtils.getDefault().getEffectiveSubId(
320                ParticipantData.DEFAULT_SELF_SUB_ID);
321        List<ApnSettingsLoader.Apn> apns;
322        boolean didLoad = false;
323        synchronized (this) {
324            apns = mApnsCache.get(subId);
325            if (apns == null) {
326                apns = new ArrayList<>();
327                mApnsCache.put(subId, apns);
328                loadLocked(subId, apnName, apns);
329                didLoad = true;
330            }
331        }
332        if (didLoad) {
333            LogUtil.i(LogUtil.BUGLE_TAG, "Loaded " + apns.size() + " APNs");
334        }
335        return apns;
336    }
337
338    private void loadLocked(final int subId, final String apnName, final List<Apn> apns) {
339        // Try Gservices first
340        loadFromGservices(apns);
341        if (apns.size() > 0) {
342            return;
343        }
344        // Try system APN table
345        loadFromSystem(subId, apnName, apns);
346        if (apns.size() > 0) {
347            return;
348        }
349        // Try local APN table
350        loadFromLocalDatabase(apnName, apns);
351        if (apns.size() <= 0) {
352            LogUtil.w(LogUtil.BUGLE_TAG, "Failed to load any APN");
353        }
354    }
355
356    /**
357     * Load from Gservices if APN setting is set in Gservices
358     *
359     * @param apns the list used to return results
360     */
361    private void loadFromGservices(final List<Apn> apns) {
362        final BugleGservices gservices = BugleGservices.get();
363        final String mmsc = gservices.getString(BugleGservicesKeys.MMS_MMSC, null);
364        if (TextUtils.isEmpty(mmsc)) {
365            return;
366        }
367        LogUtil.i(LogUtil.BUGLE_TAG, "Loading APNs from gservices");
368        final String proxy = gservices.getString(BugleGservicesKeys.MMS_PROXY_ADDRESS, null);
369        final int port = gservices.getInt(BugleGservicesKeys.MMS_PROXY_PORT, -1);
370        final Apn apn = BaseApn.from("mms", mmsc, proxy, Integer.toString(port));
371        if (apn != null) {
372            apns.add(apn);
373        }
374    }
375
376    /**
377     * Load matching APNs from telephony provider.
378     * We try different combinations of the query to work around some platform quirks.
379     *
380     * @param subId the SIM subId
381     * @param apnName the APN name to match
382     * @param apns the list used to return results
383     */
384    private void loadFromSystem(final int subId, final String apnName, final List<Apn> apns) {
385        Uri uri;
386        if (OsUtil.isAtLeastL_MR1() && subId != MmsManager.DEFAULT_SUB_ID) {
387            uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "/subId/" + subId);
388        } else {
389            uri = Telephony.Carriers.CONTENT_URI;
390        }
391        Cursor cursor = null;
392        try {
393            for (; ; ) {
394                // Try different combinations of queries. Some would work on some platforms.
395                // So we query each combination until we find one returns non-empty result.
396                cursor = querySystem(uri, true/*checkCurrent*/, apnName);
397                if (cursor != null) {
398                    break;
399                }
400                cursor = querySystem(uri, false/*checkCurrent*/, apnName);
401                if (cursor != null) {
402                    break;
403                }
404                cursor = querySystem(uri, true/*checkCurrent*/, null/*apnName*/);
405                if (cursor != null) {
406                    break;
407                }
408                cursor = querySystem(uri, false/*checkCurrent*/, null/*apnName*/);
409                break;
410            }
411        } catch (final SecurityException e) {
412            // Can't access platform APN table, return directly
413            return;
414        }
415        if (cursor == null) {
416            return;
417        }
418        try {
419            if (cursor.moveToFirst()) {
420                final ApnSettingsLoader.Apn apn = BaseApn.from(
421                        cursor.getString(COLUMN_TYPE),
422                        cursor.getString(COLUMN_MMSC),
423                        cursor.getString(COLUMN_MMSPROXY),
424                        cursor.getString(COLUMN_MMSPORT));
425                if (apn != null) {
426                    apns.add(apn);
427                }
428            }
429        } finally {
430            cursor.close();
431        }
432    }
433
434    /**
435     * Query system APN table
436     *
437     * @param uri The APN query URL to use
438     * @param checkCurrent If add "CURRENT IS NOT NULL" condition
439     * @param apnName The optional APN name for query condition
440     * @return A cursor of the query result. If a cursor is returned as not null, it is
441     *         guaranteed to contain at least one row.
442     */
443    private Cursor querySystem(final Uri uri, final boolean checkCurrent, String apnName) {
444        LogUtil.i(LogUtil.BUGLE_TAG, "Loading APNs from system, "
445                + "checkCurrent=" + checkCurrent + " apnName=" + apnName);
446        final StringBuilder selectionBuilder = new StringBuilder();
447        String[] selectionArgs = null;
448        if (checkCurrent) {
449            selectionBuilder.append(SELECTION_CURRENT);
450        }
451        apnName = trimWithNullCheck(apnName);
452        if (!TextUtils.isEmpty(apnName)) {
453            if (selectionBuilder.length() > 0) {
454                selectionBuilder.append(" AND ");
455            }
456            selectionBuilder.append(SELECTION_APN);
457            selectionArgs = new String[] { apnName };
458        }
459        try {
460            final Cursor cursor = SqliteWrapper.query(
461                    mContext,
462                    mContext.getContentResolver(),
463                    uri,
464                    APN_PROJECTION_SYSTEM,
465                    selectionBuilder.toString(),
466                    selectionArgs,
467                    null/*sortOrder*/);
468            if (cursor == null || cursor.getCount() < 1) {
469                if (cursor != null) {
470                    cursor.close();
471                }
472                LogUtil.w(LogUtil.BUGLE_TAG, "Query " + uri + " with apn " + apnName + " and "
473                        + (checkCurrent ? "checking CURRENT" : "not checking CURRENT")
474                        + " returned empty");
475                return null;
476            }
477            return cursor;
478        } catch (final SQLiteException e) {
479            LogUtil.w(LogUtil.BUGLE_TAG, "APN table query exception: " + e);
480        } catch (final SecurityException e) {
481            LogUtil.w(LogUtil.BUGLE_TAG, "Platform restricts APN table access: " + e);
482            throw e;
483        }
484        return null;
485    }
486
487    /**
488     * Load matching APNs from local APN table.
489     * We try both using the APN name and not using the APN name.
490     *
491     * @param apnName the APN name
492     * @param apns the list of results to return
493     */
494    private void loadFromLocalDatabase(final String apnName, final List<Apn> apns) {
495        LogUtil.i(LogUtil.BUGLE_TAG, "Loading APNs from local APN table");
496        final SQLiteDatabase database = ApnDatabase.getApnDatabase().getWritableDatabase();
497        final String mccMnc = PhoneUtils.getMccMncString(PhoneUtils.getDefault().getMccMnc());
498        Cursor cursor = null;
499        cursor = queryLocalDatabase(database, mccMnc, apnName);
500        if (cursor == null) {
501            cursor = queryLocalDatabase(database, mccMnc, null/*apnName*/);
502        }
503        if (cursor == null) {
504            LogUtil.w(LogUtil.BUGLE_TAG, "Could not find any APN in local table");
505            return;
506        }
507        try {
508            while (cursor.moveToNext()) {
509                final Apn apn = DatabaseApn.from(apns,
510                        cursor.getString(COLUMN_TYPE),
511                        cursor.getString(COLUMN_MMSC),
512                        cursor.getString(COLUMN_MMSPROXY),
513                        cursor.getString(COLUMN_MMSPORT),
514                        cursor.getLong(COLUMN_ID),
515                        cursor.getInt(COLUMN_CURRENT));
516                if (apn != null) {
517                    apns.add(apn);
518                }
519            }
520        } finally {
521            cursor.close();
522        }
523    }
524
525    /**
526     * Make a query of local APN table based on MCC/MNC and APN name, sorted by CURRENT
527     * column in descending order
528     *
529     * @param db the local database
530     * @param numeric the MCC/MNC string
531     * @param apnName the optional APN name to match
532     * @return the cursor of the query, null if no result
533     */
534    private static Cursor queryLocalDatabase(final SQLiteDatabase db, final String numeric,
535            final String apnName) {
536        final String selection;
537        final String[] selectionArgs;
538        if (TextUtils.isEmpty(apnName)) {
539            selection = SELECTION_NUMERIC;
540            selectionArgs = new String[] { numeric };
541        } else {
542            selection = SELECTION_NUMERIC + " AND " + SELECTION_APN;
543            selectionArgs = new String[] { numeric, apnName };
544        }
545        Cursor cursor = null;
546        try {
547            cursor = db.query(ApnDatabase.APN_TABLE, APN_PROJECTION_LOCAL, selection, selectionArgs,
548                    null/*groupBy*/, null/*having*/, ORDER_BY, null/*limit*/);
549        } catch (final SQLiteException e) {
550            LogUtil.w(LogUtil.BUGLE_TAG, "Local APN table does not exist. Try rebuilding.", e);
551            ApnDatabase.forceBuildAndLoadApnTables();
552            cursor = db.query(ApnDatabase.APN_TABLE, APN_PROJECTION_LOCAL, selection, selectionArgs,
553                    null/*groupBy*/, null/*having*/, ORDER_BY, null/*limit*/);
554        }
555        if (cursor == null || cursor.getCount() < 1) {
556            if (cursor != null) {
557                cursor.close();
558            }
559            LogUtil.w(LogUtil.BUGLE_TAG, "Query local APNs with apn " + apnName
560                    + " returned empty");
561            return null;
562        }
563        return cursor;
564    }
565
566    private static String trimWithNullCheck(final String value) {
567        return value != null ? value.trim() : null;
568    }
569
570    /**
571     * Trim leading zeros from IPv4 address strings
572     * Our base libraries will interpret that as octel..
573     * Must leave non v4 addresses and host names alone.
574     * For example, 192.168.000.010 -> 192.168.0.10
575     *
576     * @param addr a string representing an ip addr
577     * @return a string propertly trimmed
578     */
579    private static String trimV4AddrZeros(final String addr) {
580        if (addr == null) {
581            return null;
582        }
583        final String[] octets = addr.split("\\.");
584        if (octets.length != 4) {
585            return addr;
586        }
587        final StringBuilder builder = new StringBuilder(16);
588        String result = null;
589        for (int i = 0; i < 4; i++) {
590            try {
591                if (octets[i].length() > 3) {
592                    return addr;
593                }
594                builder.append(Integer.parseInt(octets[i]));
595            } catch (final NumberFormatException e) {
596                return addr;
597            }
598            if (i < 3) {
599                builder.append('.');
600            }
601        }
602        result = builder.toString();
603        return result;
604    }
605
606    /**
607     * Check if the APN contains the APN type we want
608     *
609     * @param types The string encodes a list of supported types
610     * @param requestType The type we want
611     * @return true if the input types string contains the requestType
612     */
613    public static boolean isValidApnType(final String types, final String requestType) {
614        // If APN type is unspecified, assume APN_TYPE_ALL.
615        if (TextUtils.isEmpty(types)) {
616            return true;
617        }
618        for (final String t : types.split(",")) {
619            if (t.equals(requestType) || t.equals(APN_TYPE_ALL)) {
620                return true;
621            }
622        }
623        return false;
624    }
625
626    /**
627     * Get the ID of first APN to try
628     */
629    public static String getFirstTryApn(final SQLiteDatabase database, final String mccMnc) {
630        String key = null;
631        Cursor cursor = null;
632        try {
633            cursor = queryLocalDatabase(database, mccMnc, null/*apnName*/);
634            if (cursor.moveToFirst()) {
635                key = cursor.getString(ApnDatabase.COLUMN_ID);
636            }
637        } catch (final Exception e) {
638            // Nothing to do
639        } finally {
640            if (cursor != null) {
641                cursor.close();
642            }
643        }
644        return key;
645    }
646}
647