Home | History | Annotate | Download | only in sms
      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 android.support.v7.mms.ApnSettingsLoader;
     27 import android.support.v7.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          */
     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