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 com.android.messaging.sms;
18 
19 import android.content.ContentValues;
20 import android.content.Context;
21 import android.database.Cursor;
22 import android.database.sqlite.SQLiteDatabase;
23 import android.database.sqlite.SQLiteException;
24 import android.net.Uri;
25 import android.provider.Telephony;
26 import androidx.appcompat.mms.ApnSettingsLoader;
27 import androidx.appcompat.mms.MmsManager;
28 import android.text.TextUtils;
29 import android.util.SparseArray;
30 
31 import com.android.messaging.datamodel.data.ParticipantData;
32 import com.android.messaging.mmslib.SqliteWrapper;
33 import com.android.messaging.util.BugleGservices;
34 import com.android.messaging.util.BugleGservicesKeys;
35 import com.android.messaging.util.LogUtil;
36 import com.android.messaging.util.OsUtil;
37 import com.android.messaging.util.PhoneUtils;
38 
39 import java.net.URI;
40 import java.net.URISyntaxException;
41 import java.util.ArrayList;
42 import 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  */
52 public 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          */
from(final String typesIn, final String mmscIn, final String proxyIn, final String portIn)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 
BaseApn(final String mmsc, final String proxy, final int port)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
getMmsc()108         public String getMmsc() {
109             return mMmsc;
110         }
111 
112         @Override
getMmsProxy()113         public String getMmsProxy() {
114             return mMmsProxy;
115         }
116 
117         @Override
getMmsProxyPort()118         public int getMmsProxyPort() {
119             return mMmsProxyPort;
120         }
121 
122         @Override
setSuccess()123         public void setSuccess() {
124             // Do nothing
125         }
126 
equals(final BaseApn other)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);
CURRENT_SET_VALUE.put(Telephony.Carriers.CURRENT, "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          */
from(final List<Apn> apns, final String typesIn, final String mmscIn, final String proxyIn, final String portIn, final long rowId, final int current)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 
DatabaseApn(final List<Apn> apns, final BaseApn base, final long rowId, final int current)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
getMmsc()194         public String getMmsc() {
195             return mBase.getMmsc();
196         }
197 
198         @Override
getMmsProxy()199         public String getMmsProxy() {
200             return mBase.getMmsProxy();
201         }
202 
203         @Override
getMmsProxyPort()204         public int getMmsProxyPort() {
205             return mBase.getMmsProxyPort();
206         }
207 
208         @Override
setSuccess()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          */
moveToListHead()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          */
setCurrentInDatabase()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 
equals(final BaseApn other)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 
BugleApnSettingsLoader(final Context context)312     public BugleApnSettingsLoader(final Context context) {
313         mContext = context;
314         mApnsCache = new SparseArray<>();
315     }
316 
317     @Override
get(final String apnName)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 
loadLocked(final int subId, final String apnName, final List<Apn> apns)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      */
loadFromGservices(final List<Apn> apns)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      */
loadFromSystem(final int subId, final String apnName, final List<Apn> apns)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      */
querySystem(final Uri uri, final boolean checkCurrent, String apnName)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      */
loadFromLocalDatabase(final String apnName, final List<Apn> apns)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      */
queryLocalDatabase(final SQLiteDatabase db, final String numeric, final String apnName)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 
trimWithNullCheck(final String value)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      */
trimV4AddrZeros(final String addr)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      */
isValidApnType(final String types, final String requestType)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      */
getFirstTryApn(final SQLiteDatabase database, final String mccMnc)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