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 
17 package android.support.v7.mms;
18 
19 import android.content.ContentValues;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.content.res.XmlResourceParser;
23 import android.database.Cursor;
24 import android.database.sqlite.SQLiteException;
25 import android.net.Uri;
26 import android.provider.Telephony;
27 import android.text.TextUtils;
28 import android.util.Log;
29 import android.util.SparseArray;
30 
31 import com.android.messaging.R;
32 
33 import java.net.URI;
34 import java.net.URISyntaxException;
35 import java.util.ArrayList;
36 import java.util.List;
37 
38 /**
39  * Default implementation of APN settings loader
40  */
41 class 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          */
from(final String typesIn, final String mmscIn, final String proxyIn, final String portIn)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 
BaseApn(final String mmsc, final String proxy, final int port)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
getMmsc()97         public String getMmsc() {
98             return mMmsc;
99         }
100 
101         @Override
getMmsProxy()102         public String getMmsProxy() {
103             return mMmsProxy;
104         }
105 
106         @Override
getMmsProxyPort()107         public int getMmsProxyPort() {
108             return mMmsProxyPort;
109         }
110 
111         @Override
setSuccess()112         public void setSuccess() {
113             // Do nothing
114         }
115 
equals(final BaseApn other)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          */
from(final List<Apn> apns, final String typesIn, final String mmscIn, final String proxyIn, final String portIn)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 
MemoryApn(final List<Apn> apns, final BaseApn base)158         public MemoryApn(final List<Apn> apns, final BaseApn base) {
159             mApns = apns;
160             mBase = base;
161         }
162 
163         @Override
getMmsc()164         public String getMmsc() {
165             return mBase.getMmsc();
166         }
167 
168         @Override
getMmsProxy()169         public String getMmsProxy() {
170             return mBase.getMmsProxy();
171         }
172 
173         @Override
getMmsProxyPort()174         public int getMmsProxyPort() {
175             return mBase.getMmsProxyPort();
176         }
177 
178         @Override
setSuccess()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 
equals(final BaseApn other)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 
DefaultApnSettingsLoader(final Context context)238     DefaultApnSettingsLoader(final Context context) {
239         mContext = context;
240         mApnsCache = new SparseArray<>();
241     }
242 
243     @Override
get(final String apnName)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 
loadLocked(final int subId, final String apnName, final List<Apn> apns)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      */
loadFromSystem(final int subId, final String apnName, final List<Apn> apns)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      */
querySystem(final Uri uri, final boolean checkCurrent, String apnName)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      */
loadFromResources(final int subId, final String apnName, final List<Apn> apns)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 
trimWithNullCheck(final String value)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      */
trimV4AddrZeros(final String addr)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      */
isValidApnType(final String types, final String requestType)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