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 android.support.v7.mms;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.content.res.Resources;
22import android.content.res.XmlResourceParser;
23import android.database.Cursor;
24import android.database.sqlite.SQLiteException;
25import android.net.Uri;
26import android.provider.Telephony;
27import android.text.TextUtils;
28import android.util.Log;
29import android.util.SparseArray;
30
31import com.android.messaging.R;
32
33import java.net.URI;
34import java.net.URISyntaxException;
35import java.util.ArrayList;
36import java.util.List;
37
38/**
39 * Default implementation of APN settings loader
40 */
41class DefaultApnSettingsLoader implements ApnSettingsLoader {
42    /**
43     * The base implementation of an APN
44     */
45    private static class BaseApn implements Apn {
46        /**
47         * Create a base APN from parameters
48         *
49         * @param typesIn the APN type field
50         * @param mmscIn the APN mmsc field
51         * @param proxyIn the APN mmsproxy field
52         * @param portIn the APN mmsport field
53         * @return an instance of base APN, or null if any of the parameter is invalid
54         */
55        public static BaseApn from(final String typesIn, final String mmscIn, final String proxyIn,
56                final String portIn) {
57            if (!isValidApnType(trimWithNullCheck(typesIn), APN_TYPE_MMS)) {
58                return null;
59            }
60            String mmsc = trimWithNullCheck(mmscIn);
61            if (TextUtils.isEmpty(mmsc)) {
62                return null;
63            }
64            mmsc = trimV4AddrZeros(mmsc);
65            try {
66                new URI(mmsc);
67            } catch (final URISyntaxException e) {
68                return null;
69            }
70            String mmsProxy = trimWithNullCheck(proxyIn);
71            int mmsProxyPort = 80;
72            if (!TextUtils.isEmpty(mmsProxy)) {
73                mmsProxy = trimV4AddrZeros(mmsProxy);
74                final String portString = trimWithNullCheck(portIn);
75                if (portString != null) {
76                    try {
77                        mmsProxyPort = Integer.parseInt(portString);
78                    } catch (final NumberFormatException e) {
79                        // Ignore, just use 80 to try
80                    }
81                }
82            }
83            return new BaseApn(mmsc, mmsProxy, mmsProxyPort);
84        }
85
86        private final String mMmsc;
87        private final String mMmsProxy;
88        private final int mMmsProxyPort;
89
90        public BaseApn(final String mmsc, final String proxy, final int port) {
91            mMmsc = mmsc;
92            mMmsProxy = proxy;
93            mMmsProxyPort = port;
94        }
95
96        @Override
97        public String getMmsc() {
98            return mMmsc;
99        }
100
101        @Override
102        public String getMmsProxy() {
103            return mMmsProxy;
104        }
105
106        @Override
107        public int getMmsProxyPort() {
108            return mMmsProxyPort;
109        }
110
111        @Override
112        public void setSuccess() {
113            // Do nothing
114        }
115
116        public boolean equals(final BaseApn other) {
117            return TextUtils.equals(mMmsc, other.getMmsc()) &&
118                    TextUtils.equals(mMmsProxy, other.getMmsProxy()) &&
119                    mMmsProxyPort == other.getMmsProxyPort();
120        }
121    }
122
123    /**
124     * An in-memory implementation of an APN. These APNs are organized into an in-memory list.
125     * The order of the list can be changed by the setSuccess method.
126     */
127    private static class MemoryApn implements Apn {
128        /**
129         * Create an in-memory APN loaded from resources
130         *
131         * @param apns the in-memory APN list
132         * @param typesIn the APN type field
133         * @param mmscIn the APN mmsc field
134         * @param proxyIn the APN mmsproxy field
135         * @param portIn the APN mmsport field
136         * @return an in-memory APN instance, null if there is invalid parameter
137         */
138        public static MemoryApn from(final List<Apn> apns, final String typesIn,
139                final String mmscIn, final String proxyIn, final String portIn) {
140            if (apns == null) {
141                return null;
142            }
143            final BaseApn base = BaseApn.from(typesIn, mmscIn, proxyIn, portIn);
144            if (base == null) {
145                return null;
146            }
147            for (final Apn apn : apns) {
148                if (apn instanceof MemoryApn && ((MemoryApn) apn).equals(base)) {
149                    return null;
150                }
151            }
152            return new MemoryApn(apns, base);
153        }
154
155        private final List<Apn> mApns;
156        private final BaseApn mBase;
157
158        public MemoryApn(final List<Apn> apns, final BaseApn base) {
159            mApns = apns;
160            mBase = base;
161        }
162
163        @Override
164        public String getMmsc() {
165            return mBase.getMmsc();
166        }
167
168        @Override
169        public String getMmsProxy() {
170            return mBase.getMmsProxy();
171        }
172
173        @Override
174        public int getMmsProxyPort() {
175            return mBase.getMmsProxyPort();
176        }
177
178        @Override
179        public void setSuccess() {
180            // If this is being marked as a successful APN, move it to the top of the list so
181            // next time it will be tried first
182            boolean moved = false;
183            synchronized (mApns) {
184                if (mApns.get(0) != this) {
185                    mApns.remove(this);
186                    mApns.add(0, this);
187                    moved = true;
188                }
189            }
190            if (moved) {
191                Log.d(MmsService.TAG, "Set APN ["
192                        + "MMSC=" + getMmsc() + ", "
193                        + "PROXY=" + getMmsProxy() + ", "
194                        + "PORT=" + getMmsProxyPort() + "] to be first");
195            }
196        }
197
198        public boolean equals(final BaseApn other) {
199            if (other == null) {
200                return false;
201            }
202            return mBase.equals(other);
203        }
204    }
205
206    /**
207     * APN_TYPE_ALL is a special type to indicate that this APN entry can
208     * service all data connections.
209     */
210    public static final String APN_TYPE_ALL = "*";
211    /** APN type for MMS traffic */
212    public static final String APN_TYPE_MMS = "mms";
213
214    private static final String[] APN_PROJECTION = {
215            Telephony.Carriers.TYPE,
216            Telephony.Carriers.MMSC,
217            Telephony.Carriers.MMSPROXY,
218            Telephony.Carriers.MMSPORT,
219    };
220    private static final int COLUMN_TYPE         = 0;
221    private static final int COLUMN_MMSC         = 1;
222    private static final int COLUMN_MMSPROXY     = 2;
223    private static final int COLUMN_MMSPORT      = 3;
224
225    private static final String APN_MCC = "mcc";
226    private static final String APN_MNC = "mnc";
227    private static final String APN_APN = "apn";
228    private static final String APN_TYPE = "type";
229    private static final String APN_MMSC = "mmsc";
230    private static final String APN_MMSPROXY = "mmsproxy";
231    private static final String APN_MMSPORT = "mmsport";
232
233    private final Context mContext;
234
235    // Cached APNs for subIds
236    private final SparseArray<List<Apn>> mApnsCache;
237
238    DefaultApnSettingsLoader(final Context context) {
239        mContext = context;
240        mApnsCache = new SparseArray<>();
241    }
242
243    @Override
244    public List<Apn> get(final String apnName) {
245        final int subId = Utils.getEffectiveSubscriptionId(MmsManager.DEFAULT_SUB_ID);
246        List<Apn> apns;
247        boolean didLoad = false;
248        synchronized (this) {
249            apns = mApnsCache.get(subId);
250            if (apns == null) {
251                apns = new ArrayList<>();
252                mApnsCache.put(subId, apns);
253                loadLocked(subId, apnName, apns);
254                didLoad = true;
255            }
256        }
257        if (didLoad) {
258            Log.i(MmsService.TAG, "Loaded " + apns.size() + " APNs");
259        }
260        return apns;
261    }
262
263    private void loadLocked(final int subId, final String apnName, final List<Apn> apns) {
264        // Try system APN table first
265        loadFromSystem(subId, apnName, apns);
266        if (apns.size() > 0) {
267            return;
268        }
269        // Try loading from apns.xml in resources
270        loadFromResources(subId, apnName, apns);
271        if (apns.size() > 0) {
272            return;
273        }
274        // Try resources but without APN name
275        loadFromResources(subId, null/*apnName*/, apns);
276    }
277
278    /**
279     * Load matching APNs from telephony provider.
280     * We try different combinations of the query to work around some platform quirks.
281     *
282     * @param subId the SIM subId
283     * @param apnName the APN name to match
284     * @param apns the list used to return results
285     */
286    private void loadFromSystem(final int subId, final String apnName, final List<Apn> apns) {
287        Uri uri;
288        if (Utils.supportMSim() && subId != MmsManager.DEFAULT_SUB_ID) {
289            uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "/subId/" + subId);
290        } else {
291            uri = Telephony.Carriers.CONTENT_URI;
292        }
293        Cursor cursor = null;
294        try {
295            for (; ; ) {
296                // Try different combinations of queries. Some would work on some platforms.
297                // So we query each combination until we find one returns non-empty result.
298                cursor = querySystem(uri, true/*checkCurrent*/, apnName);
299                if (cursor != null) {
300                    break;
301                }
302                cursor = querySystem(uri, false/*checkCurrent*/, apnName);
303                if (cursor != null) {
304                    break;
305                }
306                cursor = querySystem(uri, true/*checkCurrent*/, null/*apnName*/);
307                if (cursor != null) {
308                    break;
309                }
310                cursor = querySystem(uri, false/*checkCurrent*/, null/*apnName*/);
311                break;
312            }
313        } catch (final SecurityException e) {
314            // Can't access platform APN table, return directly
315            return;
316        }
317        if (cursor == null) {
318            return;
319        }
320        try {
321            if (cursor.moveToFirst()) {
322                final Apn apn = BaseApn.from(
323                        cursor.getString(COLUMN_TYPE),
324                        cursor.getString(COLUMN_MMSC),
325                        cursor.getString(COLUMN_MMSPROXY),
326                        cursor.getString(COLUMN_MMSPORT));
327                if (apn != null) {
328                    apns.add(apn);
329                }
330            }
331        } finally {
332            cursor.close();
333        }
334    }
335
336    /**
337     * Query system APN table
338     *
339     * @param uri The APN query URL to use
340     * @param checkCurrent If add "CURRENT IS NOT NULL" condition
341     * @param apnName The optional APN name for query condition
342     * @return A cursor of the query result. If a cursor is returned as not null, it is
343     *         guaranteed to contain at least one row.
344     */
345    private Cursor querySystem(final Uri uri, final boolean checkCurrent, String apnName) {
346        Log.i(MmsService.TAG, "Loading APNs from system, "
347                + "checkCurrent=" + checkCurrent + " apnName=" + apnName);
348        final StringBuilder selectionBuilder = new StringBuilder();
349        String[] selectionArgs = null;
350        if (checkCurrent) {
351            selectionBuilder.append(Telephony.Carriers.CURRENT).append(" IS NOT NULL");
352        }
353        apnName = trimWithNullCheck(apnName);
354        if (!TextUtils.isEmpty(apnName)) {
355            if (selectionBuilder.length() > 0) {
356                selectionBuilder.append(" AND ");
357            }
358            selectionBuilder.append(Telephony.Carriers.APN).append("=?");
359            selectionArgs = new String[] { apnName };
360        }
361        try {
362            final Cursor cursor = mContext.getContentResolver().query(
363                    uri,
364                    APN_PROJECTION,
365                    selectionBuilder.toString(),
366                    selectionArgs,
367                    null/*sortOrder*/);
368            if (cursor == null || cursor.getCount() < 1) {
369                if (cursor != null) {
370                    cursor.close();
371                }
372                Log.w(MmsService.TAG, "Query " + uri + " with apn " + apnName + " and "
373                        + (checkCurrent ? "checking CURRENT" : "not checking CURRENT")
374                        + " returned empty");
375                return null;
376            }
377            return cursor;
378        } catch (final SQLiteException e) {
379            Log.w(MmsService.TAG, "APN table query exception: " + e);
380        } catch (final SecurityException e) {
381            Log.w(MmsService.TAG, "Platform restricts APN table access: " + e);
382            throw e;
383        }
384        return null;
385    }
386
387    /**
388     * Find matching APNs using builtin APN list resource
389     *
390     * @param subId the SIM subId
391     * @param apnName the APN name to match
392     * @param apns the list for returning results
393     */
394    private void loadFromResources(final int subId, final String apnName, final List<Apn> apns) {
395        Log.i(MmsService.TAG, "Loading APNs from resources, apnName=" + apnName);
396        final int[] mccMnc = Utils.getMccMnc(mContext, subId);
397        if (mccMnc[0] == 0 && mccMnc[0] == 0) {
398            Log.w(MmsService.TAG, "Can not get valid mcc/mnc from system");
399            return;
400        }
401        // MCC/MNC is good, loading/querying APNs from XML
402        XmlResourceParser xml = null;
403        try {
404            xml = mContext.getResources().getXml(R.xml.apns);
405            new ApnsXmlParser(xml, new ApnsXmlParser.ApnProcessor() {
406                @Override
407                public void process(ContentValues apnValues) {
408                    final String mcc = trimWithNullCheck(apnValues.getAsString(APN_MCC));
409                    final String mnc = trimWithNullCheck(apnValues.getAsString(APN_MNC));
410                    final String apn = trimWithNullCheck(apnValues.getAsString(APN_APN));
411                    try {
412                        if (mccMnc[0] == Integer.parseInt(mcc) &&
413                                mccMnc[1] == Integer.parseInt(mnc) &&
414                                (TextUtils.isEmpty(apnName) || apnName.equalsIgnoreCase(apn))) {
415                            final String type = apnValues.getAsString(APN_TYPE);
416                            final String mmsc = apnValues.getAsString(APN_MMSC);
417                            final String mmsproxy = apnValues.getAsString(APN_MMSPROXY);
418                            final String mmsport = apnValues.getAsString(APN_MMSPORT);
419                            final Apn newApn = MemoryApn.from(apns, type, mmsc, mmsproxy, mmsport);
420                            if (newApn != null) {
421                                apns.add(newApn);
422                            }
423                        }
424                    } catch (final NumberFormatException e) {
425                        // Ignore
426                    }
427                }
428            }).parse();
429        } catch (final Resources.NotFoundException e) {
430            Log.w(MmsService.TAG, "Can not get apns.xml " + e);
431        } finally {
432            if (xml != null) {
433                xml.close();
434            }
435        }
436    }
437
438    private static String trimWithNullCheck(final String value) {
439        return value != null ? value.trim() : null;
440    }
441
442    /**
443     * Trim leading zeros from IPv4 address strings
444     * Our base libraries will interpret that as octel..
445     * Must leave non v4 addresses and host names alone.
446     * For example, 192.168.000.010 -> 192.168.0.10
447     *
448     * @param addr a string representing an ip addr
449     * @return a string propertly trimmed
450     */
451    private static String trimV4AddrZeros(final String addr) {
452        if (addr == null) {
453            return null;
454        }
455        final String[] octets = addr.split("\\.");
456        if (octets.length != 4) {
457            return addr;
458        }
459        final StringBuilder builder = new StringBuilder(16);
460        String result = null;
461        for (int i = 0; i < 4; i++) {
462            try {
463                if (octets[i].length() > 3) {
464                    return addr;
465                }
466                builder.append(Integer.parseInt(octets[i]));
467            } catch (final NumberFormatException e) {
468                return addr;
469            }
470            if (i < 3) {
471                builder.append('.');
472            }
473        }
474        result = builder.toString();
475        return result;
476    }
477
478    /**
479     * Check if the APN contains the APN type we want
480     *
481     * @param types The string encodes a list of supported types
482     * @param requestType The type we want
483     * @return true if the input types string contains the requestType
484     */
485    public static boolean isValidApnType(final String types, final String requestType) {
486        // If APN type is unspecified, assume APN_TYPE_ALL.
487        if (TextUtils.isEmpty(types)) {
488            return true;
489        }
490        for (final String t : types.split(",")) {
491            if (t.equals(requestType) || t.equals(APN_TYPE_ALL)) {
492                return true;
493            }
494        }
495        return false;
496    }
497}
498