Home | History | Annotate | Download | only in incallui
      1 /*
      2  * Copyright (C) 2006 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.incallui;
     18 
     19 import com.google.common.primitives.Longs;
     20 
     21 import android.Manifest;
     22 import android.content.AsyncQueryHandler;
     23 import android.content.ContentResolver;
     24 import android.content.Context;
     25 import android.database.Cursor;
     26 import android.database.SQLException;
     27 import android.net.Uri;
     28 import android.os.Handler;
     29 import android.os.Looper;
     30 import android.os.Message;
     31 import android.provider.ContactsContract;
     32 import android.provider.ContactsContract.Directory;
     33 import android.telephony.PhoneNumberUtils;
     34 import android.text.TextUtils;
     35 
     36 import com.android.contacts.common.ContactsUtils;
     37 import com.android.contacts.common.compat.DirectoryCompat;
     38 import com.android.contacts.common.util.PermissionsUtil;
     39 import com.android.contacts.common.util.TelephonyManagerUtils;
     40 import com.android.dialer.R;
     41 import com.android.dialer.calllog.ContactInfoHelper;
     42 import com.android.dialer.service.CachedNumberLookupService;
     43 import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo;
     44 import com.android.dialerbind.ObjectFactory;
     45 
     46 import java.io.IOException;
     47 import java.io.InputStream;
     48 import java.util.ArrayList;
     49 import java.util.Arrays;
     50 import java.util.Locale;
     51 
     52 /**
     53  * Helper class to make it easier to run asynchronous caller-id lookup queries.
     54  * @see CallerInfo
     55  *
     56  */
     57 public class CallerInfoAsyncQuery {
     58     private static final boolean DBG = false;
     59     private static final String LOG_TAG = "CallerInfoAsyncQuery";
     60 
     61     private static final int EVENT_NEW_QUERY = 1;
     62     private static final int EVENT_ADD_LISTENER = 2;
     63     private static final int EVENT_END_OF_QUEUE = 3;
     64     private static final int EVENT_EMERGENCY_NUMBER = 4;
     65     private static final int EVENT_VOICEMAIL_NUMBER = 5;
     66 
     67     private CallerInfoAsyncQueryHandler mHandler;
     68 
     69     // If the CallerInfo query finds no contacts, should we use the
     70     // PhoneNumberOfflineGeocoder to look up a "geo description"?
     71     // (TODO: This could become a flag in config.xml if it ever needs to be
     72     // configured on a per-product basis.)
     73     private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true;
     74 
     75     /**
     76      * Interface for a CallerInfoAsyncQueryHandler result return.
     77      */
     78     public interface OnQueryCompleteListener {
     79         /**
     80          * Called when the query is complete.
     81          */
     82         public void onQueryComplete(int token, Object cookie, CallerInfo ci);
     83     }
     84 
     85 
     86     /**
     87      * Wrap the cookie from the WorkerArgs with additional information needed by our
     88      * classes.
     89      */
     90     private static final class CookieWrapper {
     91         public OnQueryCompleteListener listener;
     92         public Object cookie;
     93         public int event;
     94         public String number;
     95     }
     96 
     97     /**
     98      * Simple exception used to communicate problems with the query pool.
     99      */
    100     public static class QueryPoolException extends SQLException {
    101         public QueryPoolException(String error) {
    102             super(error);
    103         }
    104     }
    105 
    106     /**
    107      * Our own implementation of the AsyncQueryHandler.
    108      */
    109     private class CallerInfoAsyncQueryHandler extends AsyncQueryHandler {
    110 
    111         @Override
    112         public void startQuery(int token, Object cookie, Uri uri, String[] projection,
    113                 String selection, String[] selectionArgs, String orderBy) {
    114             if (DBG) {
    115                 // Show stack trace with the arguments.
    116                 android.util.Log.d(LOG_TAG, "InCall: startQuery: url=" + uri +
    117                                 " projection=[" + Arrays.toString(projection) + "]" +
    118                                 " selection=" + selection + " " +
    119                                 " args=[" + Arrays.toString(selectionArgs) + "]",
    120                         new RuntimeException("STACKTRACE"));
    121             }
    122             super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy);
    123         }
    124 
    125         /**
    126          * The information relevant to each CallerInfo query.  Each query may have multiple
    127          * listeners, so each AsyncCursorInfo is associated with 2 or more CookieWrapper
    128          * objects in the queue (one with a new query event, and one with a end event, with
    129          * 0 or more additional listeners in between).
    130          */
    131         private Context mQueryContext;
    132         private Uri mQueryUri;
    133         private CallerInfo mCallerInfo;
    134 
    135         /**
    136          * Our own query worker thread.
    137          *
    138          * This thread handles the messages enqueued in the looper.  The normal sequence
    139          * of events is that a new query shows up in the looper queue, followed by 0 or
    140          * more add listener requests, and then an end request.  Of course, these requests
    141          * can be interlaced with requests from other tokens, but is irrelevant to this
    142          * handler since the handler has no state.
    143          *
    144          * Note that we depend on the queue to keep things in order; in other words, the
    145          * looper queue must be FIFO with respect to input from the synchronous startQuery
    146          * calls and output to this handleMessage call.
    147          *
    148          * This use of the queue is required because CallerInfo objects may be accessed
    149          * multiple times before the query is complete.  All accesses (listeners) must be
    150          * queued up and informed in order when the query is complete.
    151          */
    152         protected class CallerInfoWorkerHandler extends WorkerHandler {
    153             public CallerInfoWorkerHandler(Looper looper) {
    154                 super(looper);
    155             }
    156 
    157             @Override
    158             public void handleMessage(Message msg) {
    159                 WorkerArgs args = (WorkerArgs) msg.obj;
    160                 CookieWrapper cw = (CookieWrapper) args.cookie;
    161 
    162                 if (cw == null) {
    163                     // Normally, this should never be the case for calls originating
    164                     // from within this code.
    165                     // However, if there is any code that this Handler calls (such as in
    166                     // super.handleMessage) that DOES place unexpected messages on the
    167                     // queue, then we need pass these messages on.
    168                     Log.d(this, "Unexpected command (CookieWrapper is null): " + msg.what +
    169                             " ignored by CallerInfoWorkerHandler, passing onto parent.");
    170 
    171                     super.handleMessage(msg);
    172                 } else {
    173                     Log.d(this, "Processing event: " + cw.event + " token (arg1): " + msg.arg1 +
    174                             " command: " + msg.what + " query URI: " +
    175                             sanitizeUriToString(args.uri));
    176 
    177                     switch (cw.event) {
    178                         case EVENT_NEW_QUERY:
    179                             //start the sql command.
    180                             super.handleMessage(msg);
    181                             break;
    182 
    183                         // shortcuts to avoid query for recognized numbers.
    184                         case EVENT_EMERGENCY_NUMBER:
    185                         case EVENT_VOICEMAIL_NUMBER:
    186 
    187                         case EVENT_ADD_LISTENER:
    188                         case EVENT_END_OF_QUEUE:
    189                             // query was already completed, so just send the reply.
    190                             // passing the original token value back to the caller
    191                             // on top of the event values in arg1.
    192                             Message reply = args.handler.obtainMessage(msg.what);
    193                             reply.obj = args;
    194                             reply.arg1 = msg.arg1;
    195 
    196                             reply.sendToTarget();
    197 
    198                             break;
    199                         default:
    200                     }
    201                 }
    202             }
    203         }
    204 
    205 
    206         /**
    207          * Asynchronous query handler class for the contact / callerinfo object.
    208          */
    209         private CallerInfoAsyncQueryHandler(Context context) {
    210             super(context.getContentResolver());
    211         }
    212 
    213         @Override
    214         protected Handler createHandler(Looper looper) {
    215             return new CallerInfoWorkerHandler(looper);
    216         }
    217 
    218         /**
    219          * Overrides onQueryComplete from AsyncQueryHandler.
    220          *
    221          * This method takes into account the state of this class; we construct the CallerInfo
    222          * object only once for each set of listeners. When the query thread has done its work
    223          * and calls this method, we inform the remaining listeners in the queue, until we're
    224          * out of listeners.  Once we get the message indicating that we should expect no new
    225          * listeners for this CallerInfo object, we release the AsyncCursorInfo back into the
    226          * pool.
    227          */
    228         @Override
    229         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
    230             try {
    231                 Log.d(this, "##### onQueryComplete() #####   query complete for token: " + token);
    232 
    233                 //get the cookie and notify the listener.
    234                 CookieWrapper cw = (CookieWrapper) cookie;
    235                 if (cw == null) {
    236                     // Normally, this should never be the case for calls originating
    237                     // from within this code.
    238                     // However, if there is any code that calls this method, we should
    239                     // check the parameters to make sure they're viable.
    240                     Log.d(this, "Cookie is null, ignoring onQueryComplete() request.");
    241                     return;
    242                 }
    243 
    244                 if (cw.event == EVENT_END_OF_QUEUE) {
    245                     release();
    246                     return;
    247                 }
    248 
    249                 // check the token and if needed, create the callerinfo object.
    250                 if (mCallerInfo == null) {
    251                     if ((mQueryContext == null) || (mQueryUri == null)) {
    252                         throw new QueryPoolException
    253                             ("Bad context or query uri, or CallerInfoAsyncQuery already released.");
    254                     }
    255 
    256                     // adjust the callerInfo data as needed, and only if it was set from the
    257                     // initial query request.
    258                     // Change the callerInfo number ONLY if it is an emergency number or the
    259                     // voicemail number, and adjust other data (including photoResource)
    260                     // accordingly.
    261                     if (cw.event == EVENT_EMERGENCY_NUMBER) {
    262                         // Note we're setting the phone number here (refer to javadoc
    263                         // comments at the top of CallerInfo class).
    264                         mCallerInfo = new CallerInfo().markAsEmergency(mQueryContext);
    265                     } else if (cw.event == EVENT_VOICEMAIL_NUMBER) {
    266                         mCallerInfo = new CallerInfo().markAsVoiceMail(mQueryContext);
    267                     } else {
    268                         mCallerInfo = CallerInfo.getCallerInfo(mQueryContext, mQueryUri, cursor);
    269                         Log.d(this, "==> Got mCallerInfo: " + mCallerInfo);
    270 
    271                         CallerInfo newCallerInfo = CallerInfo.doSecondaryLookupIfNecessary(
    272                                 mQueryContext, cw.number, mCallerInfo);
    273                         if (newCallerInfo != mCallerInfo) {
    274                             mCallerInfo = newCallerInfo;
    275                             Log.d(this, "#####async contact look up with numeric username"
    276                                     + mCallerInfo);
    277                         }
    278 
    279                         // Final step: look up the geocoded description.
    280                         if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) {
    281                             // Note we do this only if we *don't* have a valid name (i.e. if
    282                             // no contacts matched the phone number of the incoming call),
    283                             // since that's the only case where the incoming-call UI cares
    284                             // about this field.
    285                             //
    286                             // (TODO: But if we ever want the UI to show the geoDescription
    287                             // even when we *do* match a contact, we'll need to either call
    288                             // updateGeoDescription() unconditionally here, or possibly add a
    289                             // new parameter to CallerInfoAsyncQuery.startQuery() to force
    290                             // the geoDescription field to be populated.)
    291 
    292                             if (TextUtils.isEmpty(mCallerInfo.name)) {
    293                                 // Actually when no contacts match the incoming phone number,
    294                                 // the CallerInfo object is totally blank here (i.e. no name
    295                                 // *or* phoneNumber).  So we need to pass in cw.number as
    296                                 // a fallback number.
    297                                 mCallerInfo.updateGeoDescription(mQueryContext, cw.number);
    298                             }
    299                         }
    300 
    301                         // Use the number entered by the user for display.
    302                         if (!TextUtils.isEmpty(cw.number)) {
    303                             mCallerInfo.phoneNumber = PhoneNumberUtils.formatNumber(cw.number,
    304                                     mCallerInfo.normalizedNumber,
    305                                     TelephonyManagerUtils.getCurrentCountryIso(mQueryContext,
    306                                             Locale.getDefault()));
    307                         }
    308                     }
    309 
    310                     Log.d(this, "constructing CallerInfo object for token: " + token);
    311 
    312                     //notify that we can clean up the queue after this.
    313                     CookieWrapper endMarker = new CookieWrapper();
    314                     endMarker.event = EVENT_END_OF_QUEUE;
    315                     startQuery(token, endMarker, null, null, null, null, null);
    316                 }
    317 
    318                 //notify the listener that the query is complete.
    319                 if (cw.listener != null) {
    320                     Log.d(this, "notifying listener: " + cw.listener.getClass().toString() +
    321                             " for token: " + token + mCallerInfo);
    322                     cw.listener.onQueryComplete(token, cw.cookie, mCallerInfo);
    323                 }
    324             } finally {
    325                 // The cursor may have been closed in CallerInfo.getCallerInfo()
    326                 if (cursor != null && !cursor.isClosed()) {
    327                     cursor.close();
    328                 }
    329             }
    330         }
    331     }
    332 
    333     /**
    334      * Private constructor for factory methods.
    335      */
    336     private CallerInfoAsyncQuery() {
    337     }
    338 
    339     public static void startQuery(final int token, final Context context, final CallerInfo info,
    340             final OnQueryCompleteListener listener, final Object cookie) {
    341         Log.d(LOG_TAG, "##### CallerInfoAsyncQuery startContactProviderQuery()... #####");
    342         Log.d(LOG_TAG, "- number: " + info.phoneNumber);
    343         Log.d(LOG_TAG, "- cookie: " + cookie);
    344         if (!PermissionsUtil.hasPermission(context, Manifest.permission.READ_CONTACTS)) {
    345             Log.w(LOG_TAG, "Dialer doesn't have permission to read contacts.");
    346             listener.onQueryComplete(token, cookie, info);
    347             return;
    348         }
    349 
    350         OnQueryCompleteListener contactsProviderQueryCompleteListener =
    351                 new OnQueryCompleteListener() {
    352                     @Override
    353                     public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
    354                         Log.d(LOG_TAG, "contactsProviderQueryCompleteListener done");
    355                         // If there are no other directory queries, make sure that the listener is
    356                         // notified of this result.  see b/27621628
    357                         if ((ci != null && ci.contactExists) ||
    358                             !startOtherDirectoriesQuery(token, context, info, listener, cookie)) {
    359                             if (listener != null && ci != null) {
    360                                 listener.onQueryComplete(token, cookie, ci);
    361                             }
    362                         }
    363                     }
    364                 };
    365         startDefaultDirectoryQuery(token, context, info, contactsProviderQueryCompleteListener,
    366                 cookie);
    367     }
    368 
    369     // Private methods
    370     private static CallerInfoAsyncQuery startDefaultDirectoryQuery(int token, Context context,
    371             CallerInfo info, OnQueryCompleteListener listener, Object cookie) {
    372         // Construct the URI object and query params, and start the query.
    373         Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber);
    374         return startQueryInternal(token, context, info, listener, cookie, uri);
    375     }
    376 
    377     /**
    378      * Factory method to start the query based on a CallerInfo object.
    379      *
    380      * Note: if the number contains an "@" character we treat it
    381      * as a SIP address, and look it up directly in the Data table
    382      * rather than using the PhoneLookup table.
    383      * TODO: But eventually we should expose two separate methods, one for
    384      * numbers and one for SIP addresses, and then have
    385      * PhoneUtils.startGetCallerInfo() decide which one to call based on
    386      * the phone type of the incoming connection.
    387      */
    388     private static CallerInfoAsyncQuery startQueryInternal(int token, Context context,
    389             CallerInfo info, OnQueryCompleteListener listener, Object cookie, Uri contactRef) {
    390         if (DBG) {
    391             Log.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef));
    392         }
    393 
    394         CallerInfoAsyncQuery c = new CallerInfoAsyncQuery();
    395         c.allocate(context, contactRef);
    396 
    397         //create cookieWrapper, start query
    398         CookieWrapper cw = new CookieWrapper();
    399         cw.listener = listener;
    400         cw.cookie = cookie;
    401         cw.number = info.phoneNumber;
    402 
    403         // check to see if these are recognized numbers, and use shortcuts if we can.
    404         if (PhoneNumberUtils.isLocalEmergencyNumber(context, info.phoneNumber)) {
    405             cw.event = EVENT_EMERGENCY_NUMBER;
    406         } else if (info.isVoiceMailNumber()) {
    407             cw.event = EVENT_VOICEMAIL_NUMBER;
    408         } else {
    409             cw.event = EVENT_NEW_QUERY;
    410         }
    411 
    412 
    413         String[] proejection = CallerInfo.getDefaultPhoneLookupProjection(contactRef);
    414         c.mHandler.startQuery(token,
    415                 cw,  // cookie
    416                 contactRef,  // uri
    417                 proejection, // projection
    418                 null,  // selection
    419                 null,  // selectionArgs
    420                 null);  // orderBy
    421         return c;
    422     }
    423 
    424     // Return value indicates if listener was notified.
    425     private static boolean startOtherDirectoriesQuery(int token, Context context, CallerInfo info,
    426             OnQueryCompleteListener listener, Object cookie) {
    427         long[] directoryIds = getDirectoryIds(context);
    428         int size = directoryIds.length;
    429         if (size == 0) {
    430             return false;
    431         }
    432 
    433         DirectoryQueryCompleteListenerFactory listenerFactory =
    434                 new DirectoryQueryCompleteListenerFactory(context, size, listener);
    435 
    436         // The current implementation of multiple async query runs in single handler thread
    437         // in AsyncQueryHandler.
    438         // intermediateListener.onQueryComplete is also called from the same caller thread.
    439         // TODO(b/26019872): use thread pool instead of single thread.
    440         for (int i = 0; i < size; i++) {
    441             long directoryId = directoryIds[i];
    442             Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber, directoryId);
    443             if (DBG) {
    444                 Log.d(LOG_TAG, "directoryId: " + directoryId + " uri: " + uri);
    445             }
    446             OnQueryCompleteListener intermediateListener =
    447                     listenerFactory.newListener(directoryId);
    448             startQueryInternal(token, context, info, intermediateListener, cookie, uri);
    449         }
    450         return true;
    451     }
    452 
    453     /* Directory lookup related code - START */
    454     private static final String[] DIRECTORY_PROJECTION = new String[] {Directory._ID};
    455 
    456     private static long[] getDirectoryIds(Context context) {
    457         ArrayList<Long> results = new ArrayList<>();
    458 
    459         Uri uri = Directory.CONTENT_URI;
    460         if (ContactsUtils.FLAG_N_FEATURE) {
    461             uri = Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories_enterprise");
    462         }
    463 
    464         ContentResolver cr = context.getContentResolver();
    465         Cursor cursor = cr.query(uri, DIRECTORY_PROJECTION, null, null, null);
    466         addDirectoryIdsFromCursor(cursor, results);
    467 
    468         return Longs.toArray(results);
    469     }
    470 
    471     private static void addDirectoryIdsFromCursor(Cursor cursor, ArrayList<Long> results) {
    472         if (cursor != null) {
    473             int idIndex = cursor.getColumnIndex(Directory._ID);
    474             while (cursor.moveToNext()) {
    475                 long id = cursor.getLong(idIndex);
    476                 if (DirectoryCompat.isRemoteDirectoryId(id)) {
    477                     results.add(id);
    478                 }
    479             }
    480             cursor.close();
    481         }
    482     }
    483 
    484     private static final class DirectoryQueryCompleteListenerFactory {
    485         // Make sure listener to be called once and only once
    486         private int mCount;
    487         private boolean mIsListenerCalled;
    488         private final OnQueryCompleteListener mListener;
    489         private final Context mContext;
    490         private final CachedNumberLookupService mCachedNumberLookupService =
    491                 ObjectFactory.newCachedNumberLookupService();
    492 
    493         private class DirectoryQueryCompleteListener implements OnQueryCompleteListener {
    494             private final long mDirectoryId;
    495 
    496             DirectoryQueryCompleteListener(long directoryId) {
    497                 mDirectoryId = directoryId;
    498             }
    499 
    500             @Override
    501             public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
    502                 onDirectoryQueryComplete(token, cookie, ci, mDirectoryId);
    503             }
    504         }
    505 
    506         DirectoryQueryCompleteListenerFactory(Context context, int size,
    507                 OnQueryCompleteListener listener) {
    508             mCount = size;
    509             mListener = listener;
    510             mIsListenerCalled = false;
    511             mContext = context;
    512         }
    513 
    514         private void onDirectoryQueryComplete(int token, Object cookie, CallerInfo ci,
    515                 long directoryId) {
    516             boolean shouldCallListener = false;
    517             synchronized (this) {
    518                 mCount = mCount - 1;
    519                 if (!mIsListenerCalled && (ci.contactExists || mCount == 0)) {
    520                     mIsListenerCalled = true;
    521                     shouldCallListener = true;
    522                 }
    523             }
    524 
    525             // Don't call callback in synchronized block because mListener.onQueryComplete may
    526             // take long time to complete
    527             if (shouldCallListener && mListener != null) {
    528                 addCallerInfoIntoCache(ci, directoryId);
    529                 mListener.onQueryComplete(token, cookie, ci);
    530             }
    531         }
    532 
    533         private void addCallerInfoIntoCache(CallerInfo ci, long directoryId) {
    534             if (ci.contactExists && mCachedNumberLookupService != null) {
    535                 // 1. Cache caller info
    536                 CachedContactInfo cachedContactInfo = CallerInfoUtils
    537                         .buildCachedContactInfo(mCachedNumberLookupService, ci);
    538                 String directoryLabel = mContext.getString(R.string.directory_search_label);
    539                 cachedContactInfo.setDirectorySource(directoryLabel, directoryId);
    540                 mCachedNumberLookupService.addContact(mContext, cachedContactInfo);
    541 
    542                 // 2. Cache photo
    543                 if (ci.contactDisplayPhotoUri != null && ci.normalizedNumber != null) {
    544                     try (InputStream in = mContext.getContentResolver()
    545                             .openInputStream(ci.contactDisplayPhotoUri)) {
    546                         if (in != null) {
    547                             mCachedNumberLookupService.addPhoto(mContext, ci.normalizedNumber, in);
    548                         }
    549                     } catch (IOException e) {
    550                         Log.e(LOG_TAG, "failed to fetch directory contact photo", e);
    551                     }
    552 
    553                 }
    554             }
    555         }
    556 
    557         public OnQueryCompleteListener newListener(long directoryId) {
    558             return new DirectoryQueryCompleteListener(directoryId);
    559         }
    560     }
    561     /* Directory lookup related code - END */
    562 
    563     /**
    564      * Method to create a new CallerInfoAsyncQueryHandler object, ensuring correct
    565      * state of context and uri.
    566      */
    567     private void allocate(Context context, Uri contactRef) {
    568         if ((context == null) || (contactRef == null)){
    569             throw new QueryPoolException("Bad context or query uri.");
    570         }
    571         mHandler = new CallerInfoAsyncQueryHandler(context);
    572         mHandler.mQueryContext = context;
    573         mHandler.mQueryUri = contactRef;
    574     }
    575 
    576     /**
    577      * Releases the relevant data.
    578      */
    579     private void release() {
    580         mHandler.mQueryContext = null;
    581         mHandler.mQueryUri = null;
    582         mHandler.mCallerInfo = null;
    583         mHandler = null;
    584     }
    585 
    586     private static String sanitizeUriToString(Uri uri) {
    587         if (uri != null) {
    588             String uriString = uri.toString();
    589             int indexOfLastSlash = uriString.lastIndexOf('/');
    590             if (indexOfLastSlash > 0) {
    591                 return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx";
    592             } else {
    593                 return uriString;
    594             }
    595         } else {
    596             return "";
    597         }
    598     }
    599 }
    600