Home | History | Annotate | Download | only in packageinstaller
      1 /*
      2 **
      3 ** Copyright 2013, The Android Open Source Project
      4 **
      5 ** Licensed under the Apache License, Version 2.0 (the "License");
      6 ** you may not use this file except in compliance with the License.
      7 ** You may obtain a copy of the License at
      8 **
      9 **     http://www.apache.org/licenses/LICENSE-2.0
     10 **
     11 ** Unless required by applicable law or agreed to in writing, software
     12 ** distributed under the License is distributed on an "AS IS" BASIS,
     13 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 ** See the License for the specific language governing permissions and
     15 ** limitations under the License.
     16 */
     17 package com.android.packageinstaller;
     18 
     19 import android.content.Context;
     20 import android.content.pm.PackageManager;
     21 import android.net.Uri;
     22 import android.os.AsyncTask;
     23 import android.os.Parcel;
     24 import android.os.Parcelable;
     25 import android.os.SystemClock;
     26 import android.provider.Settings;
     27 import android.util.EventLog;
     28 import android.util.Log;
     29 
     30 import java.io.BufferedInputStream;
     31 import java.io.File;
     32 import java.io.FileInputStream;
     33 import java.io.IOException;
     34 import java.io.InputStream;
     35 import java.security.MessageDigest;
     36 import java.security.NoSuchAlgorithmException;
     37 
     38 import libcore.io.IoUtils;
     39 
     40 /**
     41  * Analytics about an attempt to install a package via {@link PackageInstallerActivity}.
     42  *
     43  * <p>An instance of this class is created at the beginning of the install flow and gradually filled
     44  * as the user progresses through the flow. When the flow terminates (regardless of the reason),
     45  * {@link #setFlowFinished(byte)} is invoked which reports the installation attempt as an event
     46  * to the Event Log.
     47  */
     48 public class InstallFlowAnalytics implements Parcelable {
     49 
     50     private static final String TAG = "InstallFlowAnalytics";
     51 
     52     /** Installation has not yet terminated. */
     53     static final byte RESULT_NOT_YET_AVAILABLE = -1;
     54 
     55     /** Package successfully installed. */
     56     static final byte RESULT_SUCCESS = 0;
     57 
     58     /** Installation failed because scheme unsupported. */
     59     static final byte RESULT_FAILED_UNSUPPORTED_SCHEME = 1;
     60 
     61     /**
     62      * Installation of an APK failed because of a failure to obtain information from the provided
     63      * APK.
     64      */
     65     static final byte RESULT_FAILED_TO_GET_PACKAGE_INFO = 2;
     66 
     67     /**
     68      * Installation of an already installed package into the current user profile failed because the
     69      * specified package is not installed.
     70      */
     71     static final byte RESULT_FAILED_PACKAGE_MISSING = 3;
     72 
     73     /**
     74      * Installation failed because installation from unknown sources is prohibited by the Unknown
     75      * Sources setting.
     76      */
     77     static final byte RESULT_BLOCKED_BY_UNKNOWN_SOURCES_SETTING = 4;
     78 
     79     /** Installation cancelled by the user. */
     80     static final byte RESULT_CANCELLED_BY_USER = 5;
     81 
     82     /**
     83      * Installation failed due to {@code PackageManager} failure. PackageManager error code is
     84      * provided in {@link #mPackageManagerInstallResult}).
     85      */
     86     static final byte RESULT_PACKAGE_MANAGER_INSTALL_FAILED = 6;
     87 
     88     private static final int FLAG_INSTALLS_FROM_UNKNOWN_SOURCES_PERMITTED = 1 << 0;
     89     private static final int FLAG_INSTALL_REQUEST_FROM_UNKNOWN_SOURCE = 1 << 1;
     90     private static final int FLAG_VERIFY_APPS_ENABLED = 1 << 2;
     91     private static final int FLAG_APP_VERIFIER_INSTALLED = 1 << 3;
     92     private static final int FLAG_FILE_URI = 1 << 4;
     93     private static final int FLAG_REPLACE = 1 << 5;
     94     private static final int FLAG_SYSTEM_APP = 1 << 6;
     95     private static final int FLAG_PACKAGE_INFO_OBTAINED = 1 << 7;
     96     private static final int FLAG_INSTALL_BUTTON_CLICKED = 1 << 8;
     97     private static final int FLAG_NEW_PERMISSIONS_FOUND = 1 << 9;
     98     private static final int FLAG_PERMISSIONS_DISPLAYED = 1 << 10;
     99     private static final int FLAG_NEW_PERMISSIONS_DISPLAYED = 1 << 11;
    100     private static final int FLAG_ALL_PERMISSIONS_DISPLAYED = 1 << 12;
    101 
    102     /**
    103      * Information about this flow expressed as a collection of flags. See {@code FLAG_...}
    104      * constants.
    105      */
    106     private int mFlags;
    107 
    108     /** Outcome of the flow. See {@code RESULT_...} constants. */
    109     private byte mResult = RESULT_NOT_YET_AVAILABLE;
    110 
    111     /**
    112      * Result code returned by {@code PackageManager} to install the package or {@code 0} if
    113      * {@code PackageManager} has not yet been invoked to install the package.
    114      */
    115     private int mPackageManagerInstallResult;
    116 
    117     /**
    118      * Time instant when the installation request arrived, measured in elapsed realtime
    119      * milliseconds. See {@link SystemClock#elapsedRealtime()}.
    120      */
    121     private long mStartTimestampMillis;
    122 
    123     /**
    124      * Time instant when the information about the package being installed was obtained, measured in
    125      * elapsed realtime milliseconds. See {@link SystemClock#elapsedRealtime()}.
    126      */
    127     private long mPackageInfoObtainedTimestampMillis;
    128 
    129     /**
    130      * Time instant when the user clicked the Install button, measured in elapsed realtime
    131      * milliseconds. See {@link SystemClock#elapsedRealtime()}. This field is only valid if the
    132      * Install button has been clicked, as signaled by {@link #FLAG_INSTALL_BUTTON_CLICKED}.
    133      */
    134     private long mInstallButtonClickTimestampMillis;
    135 
    136     /**
    137      * Time instant when this flow terminated, measured in elapsed realtime milliseconds. See
    138      * {@link SystemClock#elapsedRealtime()}.
    139      */
    140     private long mEndTimestampMillis;
    141 
    142     /** URI of the package being installed. */
    143     private String mPackageUri;
    144 
    145     /** Whether this attempt has been logged to the Event Log. */
    146     private boolean mLogged;
    147 
    148     private Context mContext;
    149 
    150     public static final Parcelable.Creator<InstallFlowAnalytics> CREATOR =
    151             new Parcelable.Creator<InstallFlowAnalytics>() {
    152         @Override
    153         public InstallFlowAnalytics createFromParcel(Parcel in) {
    154             return new InstallFlowAnalytics(in);
    155         }
    156 
    157         @Override
    158         public InstallFlowAnalytics[] newArray(int size) {
    159             return new InstallFlowAnalytics[size];
    160         }
    161     };
    162 
    163     public InstallFlowAnalytics() {}
    164 
    165     public InstallFlowAnalytics(Parcel in) {
    166         mFlags = in.readInt();
    167         mResult = in.readByte();
    168         mPackageManagerInstallResult = in.readInt();
    169         mStartTimestampMillis = in.readLong();
    170         mPackageInfoObtainedTimestampMillis = in.readLong();
    171         mInstallButtonClickTimestampMillis = in.readLong();
    172         mEndTimestampMillis = in.readLong();
    173         mPackageUri = in.readString();
    174         mLogged = readBoolean(in);
    175     }
    176 
    177     @Override
    178     public void writeToParcel(Parcel dest, int flags) {
    179         dest.writeInt(mFlags);
    180         dest.writeByte(mResult);
    181         dest.writeInt(mPackageManagerInstallResult);
    182         dest.writeLong(mStartTimestampMillis);
    183         dest.writeLong(mPackageInfoObtainedTimestampMillis);
    184         dest.writeLong(mInstallButtonClickTimestampMillis);
    185         dest.writeLong(mEndTimestampMillis);
    186         dest.writeString(mPackageUri);
    187         writeBoolean(dest, mLogged);
    188     }
    189 
    190     private static void writeBoolean(Parcel dest, boolean value) {
    191         dest.writeByte((byte) (value ? 1 : 0));
    192     }
    193 
    194     private static boolean readBoolean(Parcel dest) {
    195         return dest.readByte() != 0;
    196     }
    197 
    198     @Override
    199     public int describeContents() {
    200         return 0;
    201     }
    202 
    203     void setContext(Context context) {
    204         mContext = context;
    205     }
    206 
    207     /** Sets whether the Unknown Sources setting is checked. */
    208     void setInstallsFromUnknownSourcesPermitted(boolean permitted) {
    209         setFlagState(FLAG_INSTALLS_FROM_UNKNOWN_SOURCES_PERMITTED, permitted);
    210     }
    211 
    212     /** Gets whether the Unknown Sources setting is checked. */
    213     private boolean isInstallsFromUnknownSourcesPermitted() {
    214         return isFlagSet(FLAG_INSTALLS_FROM_UNKNOWN_SOURCES_PERMITTED);
    215     }
    216 
    217     /** Sets whether this install attempt is from an unknown source. */
    218     void setInstallRequestFromUnknownSource(boolean unknownSource) {
    219         setFlagState(FLAG_INSTALL_REQUEST_FROM_UNKNOWN_SOURCE, unknownSource);
    220     }
    221 
    222     /** Gets whether this install attempt is from an unknown source. */
    223     private boolean isInstallRequestFromUnknownSource() {
    224         return isFlagSet(FLAG_INSTALL_REQUEST_FROM_UNKNOWN_SOURCE);
    225     }
    226 
    227     /** Sets whether app verification is enabled. */
    228     void setVerifyAppsEnabled(boolean enabled) {
    229         setFlagState(FLAG_VERIFY_APPS_ENABLED, enabled);
    230     }
    231 
    232     /** Gets whether app verification is enabled. */
    233     private boolean isVerifyAppsEnabled() {
    234         return isFlagSet(FLAG_VERIFY_APPS_ENABLED);
    235     }
    236 
    237     /** Sets whether at least one app verifier is installed. */
    238     void setAppVerifierInstalled(boolean installed) {
    239         setFlagState(FLAG_APP_VERIFIER_INSTALLED, installed);
    240     }
    241 
    242     /** Gets whether at least one app verifier is installed. */
    243     private boolean isAppVerifierInstalled() {
    244         return isFlagSet(FLAG_APP_VERIFIER_INSTALLED);
    245     }
    246 
    247     /**
    248      * Sets whether an APK file is being installed.
    249      *
    250      * @param fileUri {@code true} if an APK file is being installed, {@code false} if an already
    251      *        installed package is being installed to this user profile.
    252      */
    253     void setFileUri(boolean fileUri) {
    254         setFlagState(FLAG_FILE_URI, fileUri);
    255     }
    256 
    257     /**
    258      * Sets the URI of the package being installed.
    259      */
    260     void setPackageUri(String packageUri) {
    261         mPackageUri = packageUri;
    262     }
    263 
    264     /**
    265      * Gets whether an APK file is being installed.
    266      *
    267      * @return {@code true} if an APK file is being installed, {@code false} if an already
    268      *         installed package is being installed to this user profile.
    269      */
    270     private boolean isFileUri() {
    271         return isFlagSet(FLAG_FILE_URI);
    272     }
    273 
    274     /** Sets whether this is an attempt to replace an existing package. */
    275     void setReplace(boolean replace) {
    276         setFlagState(FLAG_REPLACE, replace);
    277     }
    278 
    279     /** Gets whether this is an attempt to replace an existing package. */
    280     private boolean isReplace() {
    281         return isFlagSet(FLAG_REPLACE);
    282     }
    283 
    284     /** Sets whether the package being updated is a system package. */
    285     void setSystemApp(boolean systemApp) {
    286         setFlagState(FLAG_SYSTEM_APP, systemApp);
    287     }
    288 
    289     /** Gets whether the package being updated is a system package. */
    290     private boolean isSystemApp() {
    291         return isFlagSet(FLAG_SYSTEM_APP);
    292     }
    293 
    294     /**
    295      * Sets whether the package being installed is requesting more permissions than the already
    296      * installed version of the package.
    297      */
    298     void setNewPermissionsFound(boolean found) {
    299         setFlagState(FLAG_NEW_PERMISSIONS_FOUND, found);
    300     }
    301 
    302     /**
    303      * Gets whether the package being installed is requesting more permissions than the already
    304      * installed version of the package.
    305      */
    306     private boolean isNewPermissionsFound() {
    307         return isFlagSet(FLAG_NEW_PERMISSIONS_FOUND);
    308     }
    309 
    310     /** Sets whether permissions were displayed to the user. */
    311     void setPermissionsDisplayed(boolean displayed) {
    312         setFlagState(FLAG_PERMISSIONS_DISPLAYED, displayed);
    313     }
    314 
    315     /** Gets whether permissions were displayed to the user. */
    316     private boolean isPermissionsDisplayed() {
    317         return isFlagSet(FLAG_PERMISSIONS_DISPLAYED);
    318     }
    319 
    320     /**
    321      * Sets whether new permissions were displayed to the user (if permissions were displayed at
    322      * all).
    323      */
    324     void setNewPermissionsDisplayed(boolean displayed) {
    325         setFlagState(FLAG_NEW_PERMISSIONS_DISPLAYED, displayed);
    326     }
    327 
    328     /**
    329      * Gets whether new permissions were displayed to the user (if permissions were displayed at
    330      * all).
    331      */
    332     private boolean isNewPermissionsDisplayed() {
    333         return isFlagSet(FLAG_NEW_PERMISSIONS_DISPLAYED);
    334     }
    335 
    336     /**
    337      * Sets whether all permissions were displayed to the user (if permissions were displayed at
    338      * all).
    339      */
    340     void setAllPermissionsDisplayed(boolean displayed) {
    341         setFlagState(FLAG_ALL_PERMISSIONS_DISPLAYED, displayed);
    342     }
    343 
    344     /**
    345      * Gets whether all permissions were displayed to the user (if permissions were displayed at
    346      * all).
    347      */
    348     private boolean isAllPermissionsDisplayed() {
    349         return isFlagSet(FLAG_ALL_PERMISSIONS_DISPLAYED);
    350     }
    351 
    352     /**
    353      * Sets the time instant when the installation request arrived, measured in elapsed realtime
    354      * milliseconds. See {@link SystemClock#elapsedRealtime()}.
    355      */
    356     void setStartTimestampMillis(long timestampMillis) {
    357         mStartTimestampMillis = timestampMillis;
    358     }
    359 
    360     /**
    361      * Records that the information about the package info has been obtained or that there has been
    362      * a failure to obtain the information.
    363      */
    364     void setPackageInfoObtained() {
    365         setFlagState(FLAG_PACKAGE_INFO_OBTAINED, true);
    366         mPackageInfoObtainedTimestampMillis = SystemClock.elapsedRealtime();
    367     }
    368 
    369     /**
    370      * Checks whether the information about the package info has been obtained or that there has
    371      * been a failure to obtain the information.
    372      */
    373     private boolean isPackageInfoObtained() {
    374         return isFlagSet(FLAG_PACKAGE_INFO_OBTAINED);
    375     }
    376 
    377     /**
    378      * Records that the Install button has been clicked.
    379      */
    380     void setInstallButtonClicked() {
    381         setFlagState(FLAG_INSTALL_BUTTON_CLICKED, true);
    382         mInstallButtonClickTimestampMillis = SystemClock.elapsedRealtime();
    383     }
    384 
    385     /**
    386      * Checks whether the Install button has been clicked.
    387      */
    388     private boolean isInstallButtonClicked() {
    389         return isFlagSet(FLAG_INSTALL_BUTTON_CLICKED);
    390     }
    391 
    392     /**
    393      * Marks this flow as finished due to {@code PackageManager} succeeding or failing to install
    394      * the package and reports this to the Event Log.
    395      */
    396     void setFlowFinishedWithPackageManagerResult(int packageManagerResult) {
    397         mPackageManagerInstallResult = packageManagerResult;
    398         if (packageManagerResult == PackageManager.INSTALL_SUCCEEDED) {
    399             setFlowFinished(
    400                     InstallFlowAnalytics.RESULT_SUCCESS);
    401         } else {
    402             setFlowFinished(
    403                     InstallFlowAnalytics.RESULT_PACKAGE_MANAGER_INSTALL_FAILED);
    404         }
    405     }
    406 
    407     /**
    408      * Marks this flow as finished and reports this to the Event Log.
    409      */
    410     void setFlowFinished(byte result) {
    411         if (mLogged) {
    412             return;
    413         }
    414         mResult = result;
    415         mEndTimestampMillis = SystemClock.elapsedRealtime();
    416         writeToEventLog();
    417     }
    418 
    419     private void writeToEventLog() {
    420         byte packageManagerInstallResultByte = 0;
    421         if (mResult == RESULT_PACKAGE_MANAGER_INSTALL_FAILED) {
    422             // PackageManager install error codes are negative, starting from -1 and going to
    423             // -111 (at the moment). We thus store them in negated form.
    424             packageManagerInstallResultByte = clipUnsignedValueToUnsignedByte(
    425                     -mPackageManagerInstallResult);
    426         }
    427 
    428         final int resultAndFlags = (mResult & 0xff)
    429                 | ((packageManagerInstallResultByte & 0xff) << 8)
    430                 | ((mFlags & 0xffff) << 16);
    431 
    432         // Total elapsed time from start to end, in milliseconds.
    433         final int totalElapsedTime =
    434                 clipUnsignedLongToUnsignedInt(mEndTimestampMillis - mStartTimestampMillis);
    435 
    436         // Total elapsed time from start till information about the package being installed was
    437         // obtained, in milliseconds.
    438         final int elapsedTimeTillPackageInfoObtained = (isPackageInfoObtained())
    439                 ? clipUnsignedLongToUnsignedInt(
    440                         mPackageInfoObtainedTimestampMillis - mStartTimestampMillis)
    441                 : 0;
    442 
    443         // Total elapsed time from start till Install button clicked, in milliseconds
    444         // milliseconds.
    445         final int elapsedTimeTillInstallButtonClick = (isInstallButtonClicked())
    446                 ? clipUnsignedLongToUnsignedInt(
    447                             mInstallButtonClickTimestampMillis - mStartTimestampMillis)
    448                 : 0;
    449 
    450         // If this user has consented to app verification, augment the logged event with the hash of
    451         // the contents of the APK.
    452         if (((mFlags & FLAG_FILE_URI) != 0)
    453                 && ((mFlags & FLAG_VERIFY_APPS_ENABLED) != 0)
    454                 && (isUserConsentToVerifyAppsGranted())) {
    455             // Log the hash of the APK's contents.
    456             // Reading the APK may take a while -- perform in background.
    457             AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
    458                 @Override
    459                 public void run() {
    460                     byte[] digest = null;
    461                     try {
    462                         digest = getPackageContentsDigest();
    463                     } catch (IOException e) {
    464                         Log.w(TAG, "Failed to hash APK contents", e);
    465                     } finally {
    466                         String digestHex = (digest != null)
    467                                 ? IntegralToString.bytesToHexString(digest, false)
    468                                 : "";
    469                         EventLogTags.writeInstallPackageAttempt(
    470                                 resultAndFlags,
    471                                 totalElapsedTime,
    472                                 elapsedTimeTillPackageInfoObtained,
    473                                 elapsedTimeTillInstallButtonClick,
    474                                 digestHex);
    475                     }
    476                 }
    477             });
    478         } else {
    479             // Do not log the hash of the APK's contents
    480             EventLogTags.writeInstallPackageAttempt(
    481                     resultAndFlags,
    482                     totalElapsedTime,
    483                     elapsedTimeTillPackageInfoObtained,
    484                     elapsedTimeTillInstallButtonClick,
    485                     "");
    486         }
    487         mLogged = true;
    488 
    489         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    490             Log.v(TAG, "Analytics:"
    491                     + "\n\tinstallsFromUnknownSourcesPermitted: "
    492                         + isInstallsFromUnknownSourcesPermitted()
    493                     + "\n\tinstallRequestFromUnknownSource: " + isInstallRequestFromUnknownSource()
    494                     + "\n\tverifyAppsEnabled: " + isVerifyAppsEnabled()
    495                     + "\n\tappVerifierInstalled: " + isAppVerifierInstalled()
    496                     + "\n\tfileUri: " + isFileUri()
    497                     + "\n\treplace: " + isReplace()
    498                     + "\n\tsystemApp: " + isSystemApp()
    499                     + "\n\tpackageInfoObtained: " + isPackageInfoObtained()
    500                     + "\n\tinstallButtonClicked: " + isInstallButtonClicked()
    501                     + "\n\tpermissionsDisplayed: " + isPermissionsDisplayed()
    502                     + "\n\tnewPermissionsDisplayed: " + isNewPermissionsDisplayed()
    503                     + "\n\tallPermissionsDisplayed: " + isAllPermissionsDisplayed()
    504                     + "\n\tnewPermissionsFound: " + isNewPermissionsFound()
    505                     + "\n\tresult: " + mResult
    506                     + "\n\tpackageManagerInstallResult: " + mPackageManagerInstallResult
    507                     + "\n\ttotalDuration: " + (mEndTimestampMillis - mStartTimestampMillis) + " ms"
    508                     + "\n\ttimeTillPackageInfoObtained: "
    509                         + ((isPackageInfoObtained())
    510                             ? ((mPackageInfoObtainedTimestampMillis - mStartTimestampMillis)
    511                                     + " ms")
    512                             : "n/a")
    513                     + "\n\ttimeTillInstallButtonClick: "
    514                         + ((isInstallButtonClicked())
    515                             ? ((mInstallButtonClickTimestampMillis - mStartTimestampMillis) + " ms")
    516                             : "n/a"));
    517             Log.v(TAG, "Wrote to Event Log: 0x" + Long.toString(resultAndFlags & 0xffffffffL, 16)
    518                     + ", " + totalElapsedTime
    519                     + ", " + elapsedTimeTillPackageInfoObtained
    520                     + ", " + elapsedTimeTillInstallButtonClick);
    521         }
    522     }
    523 
    524     private static final byte clipUnsignedValueToUnsignedByte(long value) {
    525         if (value < 0) {
    526             return 0;
    527         } else if (value > 0xff) {
    528             return (byte) 0xff;
    529         } else {
    530             return (byte) value;
    531         }
    532     }
    533 
    534     private static final int clipUnsignedLongToUnsignedInt(long value) {
    535         if (value < 0) {
    536             return 0;
    537         } else if (value > 0xffffffffL) {
    538             return 0xffffffff;
    539         } else {
    540             return (int) value;
    541         }
    542     }
    543 
    544     /**
    545      * Sets or clears the specified flag in the {@link #mFlags} field.
    546      */
    547     private void setFlagState(int flag, boolean set) {
    548         if (set) {
    549             mFlags |= flag;
    550         } else {
    551             mFlags &= ~flag;
    552         }
    553     }
    554 
    555     /**
    556      * Checks whether the specified flag is set in the {@link #mFlags} field.
    557      */
    558     private boolean isFlagSet(int flag) {
    559         return (mFlags & flag) == flag;
    560     }
    561 
    562     /**
    563      * Checks whether the user has consented to app verification.
    564      */
    565     private boolean isUserConsentToVerifyAppsGranted() {
    566         return Settings.Secure.getInt(
    567                 mContext.getContentResolver(),
    568                 Settings.Secure.PACKAGE_VERIFIER_USER_CONSENT, 0) != 0;
    569     }
    570 
    571     /**
    572      * Gets the digest of the contents of the package being installed.
    573      */
    574     private byte[] getPackageContentsDigest() throws IOException {
    575         File file = new File(Uri.parse(mPackageUri).getPath());
    576         return getSha256ContentsDigest(file);
    577     }
    578 
    579     /**
    580      * Gets the SHA-256 digest of the contents of the specified file.
    581      */
    582     private static byte[] getSha256ContentsDigest(File file) throws IOException {
    583         MessageDigest digest;
    584         try {
    585             digest = MessageDigest.getInstance("SHA-256");
    586         } catch (NoSuchAlgorithmException e) {
    587             throw new RuntimeException("SHA-256 not available", e);
    588         }
    589 
    590         byte[] buf = new byte[8192];
    591         InputStream in = null;
    592         try {
    593             in = new BufferedInputStream(new FileInputStream(file), buf.length);
    594             int chunkSize;
    595             while ((chunkSize = in.read(buf)) != -1) {
    596                 digest.update(buf, 0, chunkSize);
    597             }
    598         } finally {
    599             IoUtils.closeQuietly(in);
    600         }
    601         return digest.digest();
    602     }
    603 }