Home | History | Annotate | Download | only in updater
      1 /*
      2  * Copyright (C) 2017 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 package com.android.timezone.updater;
     17 
     18 import android.app.timezone.Callback;
     19 import android.app.timezone.DistroFormatVersion;
     20 import android.app.timezone.DistroRulesVersion;
     21 import android.app.timezone.RulesManager;
     22 import android.app.timezone.RulesState;
     23 import android.app.timezone.RulesUpdaterContract;
     24 import android.content.BroadcastReceiver;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.pm.ApplicationInfo;
     28 import android.content.pm.PackageManager;
     29 import android.content.pm.ProviderInfo;
     30 import android.database.Cursor;
     31 import android.net.Uri;
     32 import android.os.ParcelFileDescriptor;
     33 import android.provider.TimeZoneRulesDataContract;
     34 import android.util.Log;
     35 
     36 import java.io.File;
     37 import java.io.FileInputStream;
     38 import java.io.FileNotFoundException;
     39 import java.io.FileOutputStream;
     40 import java.io.IOException;
     41 import java.io.InputStream;
     42 import java.util.Arrays;
     43 import libcore.io.Streams;
     44 
     45 /**
     46  * A broadcast receiver triggered by an
     47  * {@link RulesUpdaterContract#ACTION_TRIGGER_RULES_UPDATE_CHECK intent} from the system server in
     48  * response to the installation/replacement/uninstallation of a time zone data app.
     49  *
     50  * <p>The trigger intent contains a {@link RulesUpdaterContract#EXTRA_CHECK_TOKEN byte[] check
     51  * token} which must be returned to the system server {@link RulesManager} API via one of the
     52  * {@link RulesManager#requestInstall(ParcelFileDescriptor, byte[], Callback) install},
     53  * {@link RulesManager#requestUninstall(byte[], Callback)} or
     54  * {@link RulesManager#requestNothing(byte[], boolean)} methods.
     55  *
     56  * <p>The RulesCheckReceiver is responsible for handling the operation requested by the data app.
     57  * The data app makes its payload available via a {@link TimeZoneRulesDataContract specified}
     58  * {@link android.content.ContentProvider} with the URI {@link TimeZoneRulesDataContract#AUTHORITY}.
     59  *
     60  * <p>If the {@link TimeZoneRulesDataContract.Operation#COLUMN_TYPE operation type} is an
     61  * {@link TimeZoneRulesDataContract.Operation#TYPE_INSTALL install request}, then the time zone data
     62  * format {@link TimeZoneRulesDataContract.Operation#COLUMN_DISTRO_MAJOR_VERSION major version} and
     63  * {@link TimeZoneRulesDataContract.Operation#COLUMN_DISTRO_MINOR_VERSION minor version}, the
     64  * {@link TimeZoneRulesDataContract.Operation#COLUMN_RULES_VERSION IANA rules version}, and the
     65  * {@link TimeZoneRulesDataContract.Operation#COLUMN_REVISION revision} are checked to see if they
     66  * can be applied to the device. If the data is valid the {@link RulesCheckReceiver} will obtain
     67  * the payload from the data app content provider via
     68  * {@link android.content.ContentProvider#openFile(Uri, String)} and pass the data to the system
     69  * server for installation via the
     70  * {@link RulesManager#requestInstall(ParcelFileDescriptor, byte[], Callback)}.
     71  */
     72 public class RulesCheckReceiver extends BroadcastReceiver {
     73     final static String TAG = "RulesCheckReceiver";
     74 
     75     private RulesManager mRulesManager;
     76 
     77     @Override
     78     public void onReceive(Context context, Intent intent) {
     79         // No need to make this synchronized, onReceive() is called on the main thread, there's no
     80         // important object state that could be corrupted and the check token allows for ordering
     81         // issues.
     82         if (!RulesUpdaterContract.ACTION_TRIGGER_RULES_UPDATE_CHECK.equals(intent.getAction())) {
     83             // Unknown. Do nothing.
     84             Log.w(TAG, "Unrecognized intent action received: " + intent
     85                     + ", action=" + intent.getAction());
     86             return;
     87         }
     88         mRulesManager = (RulesManager) context.getSystemService("timezone");
     89 
     90         byte[] token = intent.getByteArrayExtra(RulesUpdaterContract.EXTRA_CHECK_TOKEN);
     91         EventLogTags.writeTimezoneCheckTriggerReceived(Arrays.toString(token));
     92 
     93         if (shouldUninstallCurrentInstall(context)) {
     94             Log.i(TAG, "Device should be returned to having no time zone distro installed, issuing"
     95                     + " uninstall request");
     96             // Uninstall is a no-op if nothing is installed.
     97             handleUninstall(token);
     98             return;
     99         }
    100 
    101         // Note: We rely on the system server to check that the configured data application is the
    102         // one that exposes the content provider with the well-known authority, and is a privileged
    103         // application as required. It is *not* checked here and it is assumed the updater can trust
    104         // the data application.
    105 
    106         // Obtain the information about what the data app is telling us to do.
    107         DistroOperation operation = getOperation(context, token);
    108         if (operation == null) {
    109             Log.w(TAG, "Unable to read time zone operation. Halting check.");
    110             boolean success = true; // No point in retrying.
    111             handleCheckComplete(token, success);
    112             return;
    113         }
    114 
    115         // Try to do what the data app asked.
    116         Log.d(TAG, "Time zone operation: " + operation + " received.");
    117         switch (operation.mType) {
    118             case TimeZoneRulesDataContract.Operation.TYPE_NO_OP:
    119                 // No-op. Just acknowledge the check.
    120                 handleCheckComplete(token, true /* success */);
    121                 break;
    122             case TimeZoneRulesDataContract.Operation.TYPE_UNINSTALL:
    123                 handleUninstall(token);
    124                 break;
    125             case TimeZoneRulesDataContract.Operation.TYPE_INSTALL:
    126                 handleCopyAndInstall(context, token, operation.mDistroFormatVersion,
    127                         operation.mDistroRulesVersion);
    128                 break;
    129             default:
    130                 Log.w(TAG, "Unknown time zone operation: " + operation
    131                         + " received. Halting check.");
    132                 final boolean success = true; // No point in retrying.
    133                 handleCheckComplete(token, success);
    134         }
    135     }
    136 
    137     private boolean shouldUninstallCurrentInstall(Context context) {
    138         int flags = PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
    139         PackageManager packageManager = context.getPackageManager();
    140         ProviderInfo providerInfo =
    141                 packageManager.resolveContentProvider(TimeZoneRulesDataContract.AUTHORITY, flags);
    142         if (providerInfo == null || providerInfo.applicationInfo == null) {
    143             Log.w(TAG, "No package/application info available for content provider "
    144                     + TimeZoneRulesDataContract.AUTHORITY);
    145             // Something has gone wrong. Trying to return the device to clean is a reasonable
    146             // response.
    147             return true;
    148         }
    149 
    150         // If the data app is the one from /system, we can treat this as "uninstall": if nothing
    151         // is installed then the system will treat this as a no-op, and if something is installed
    152         // this will stage an uninstall.
    153         // We could install the distro from an app contained in the system image but we assume it's
    154         // going to contain the same time zone data as in /system and would be a no op.
    155 
    156         ApplicationInfo applicationInfo = providerInfo.applicationInfo;
    157         // isPrivilegedApp() => initial install directory for app /system/priv-app (required)
    158         // isUpdatedSystemApp() => app has been replaced by an updated version that resides in /data
    159         return applicationInfo.isPrivilegedApp() && !applicationInfo.isUpdatedSystemApp();
    160     }
    161 
    162     private DistroOperation getOperation(Context context, byte[] tokenBytes) {
    163         EventLogTags.writeTimezoneCheckReadFromDataApp(Arrays.toString(tokenBytes));
    164         Cursor c = context.getContentResolver()
    165                 .query(TimeZoneRulesDataContract.Operation.CONTENT_URI,
    166                         new String[] {
    167                                 TimeZoneRulesDataContract.Operation.COLUMN_TYPE,
    168                                 TimeZoneRulesDataContract.Operation.COLUMN_DISTRO_MAJOR_VERSION,
    169                                 TimeZoneRulesDataContract.Operation.COLUMN_DISTRO_MINOR_VERSION,
    170                                 TimeZoneRulesDataContract.Operation.COLUMN_RULES_VERSION,
    171                                 TimeZoneRulesDataContract.Operation.COLUMN_REVISION
    172                         },
    173                         null /* selection */, null /* selectionArgs */, null /* sortOrder */);
    174         try (Cursor cursor = c) {
    175             if (cursor == null) {
    176                 Log.e(TAG, "Query returned null");
    177                 return null;
    178             }
    179             if (!cursor.moveToFirst()) {
    180                 Log.e(TAG, "Query returned empty results");
    181                 return null;
    182             }
    183 
    184             try {
    185                 String type = cursor.getString(0);
    186                 DistroFormatVersion distroFormatVersion = null;
    187                 DistroRulesVersion distroRulesVersion = null;
    188                 if (TimeZoneRulesDataContract.Operation.TYPE_INSTALL.equals(type)) {
    189                     distroFormatVersion = new DistroFormatVersion(cursor.getInt(1),
    190                             cursor.getInt(2));
    191                     distroRulesVersion = new DistroRulesVersion(cursor.getString(3),
    192                             cursor.getInt(4));
    193                 }
    194                 return new DistroOperation(type, distroFormatVersion, distroRulesVersion);
    195             } catch (Exception e) {
    196                 Log.e(TAG, "Error looking up distro operation / version", e);
    197                 return null;
    198             }
    199         }
    200     }
    201 
    202     private void handleCopyAndInstall(Context context, byte[] checkToken,
    203             DistroFormatVersion distroFormatVersion, DistroRulesVersion distroRulesVersion) {
    204         // Decide whether to proceed with the install.
    205         RulesState rulesState = mRulesManager.getRulesState();
    206         if (!rulesState.isDistroFormatVersionSupported(distroFormatVersion)
    207             || rulesState.isSystemVersionNewerThan(distroRulesVersion)) {
    208             Log.d(TAG, "Candidate distro is not supported or is not better than system version.");
    209             // Nothing to do.
    210             handleCheckComplete(checkToken, true /* success */);
    211             return;
    212         }
    213 
    214         ParcelFileDescriptor inputFileDescriptor = getDistroParcelFileDescriptor(context);
    215         if (inputFileDescriptor == null) {
    216             Log.e(TAG, "No local file created for distro. Halting.");
    217             return;
    218         }
    219 
    220         // Copying the ParcelFileDescriptor to a local file proves we can read it before passing it
    221         // on to the next stage. It also ensures that we have a hermetic copy of the data we know
    222         // the originating content provider cannot modify unexpectedly. If the next stage wants to
    223         // "seek" the ParcelFileDescriptor it can do so with fewer processes affected.
    224         File file = copyDataToLocalFile(context, inputFileDescriptor);
    225         if (file == null) {
    226             Log.e(TAG, "Failed to copy distro data to a file.");
    227             // It's possible this may get better if the problem is related to storage space so we
    228             // signal success := false so it may be retried.
    229             boolean success = false;
    230             handleCheckComplete(checkToken, success);
    231             return;
    232         }
    233         handleInstall(checkToken, file);
    234     }
    235 
    236     private static ParcelFileDescriptor getDistroParcelFileDescriptor(Context context) {
    237         ParcelFileDescriptor inputFileDescriptor;
    238         try {
    239             inputFileDescriptor = context.getContentResolver().openFileDescriptor(
    240                     TimeZoneRulesDataContract.Operation.CONTENT_URI, "r");
    241             if (inputFileDescriptor == null) {
    242                 throw new FileNotFoundException("ContentProvider returned null");
    243             }
    244         } catch (FileNotFoundException e) {
    245             Log.e(TAG, "Unable to open file descriptor"
    246                     + TimeZoneRulesDataContract.Operation.CONTENT_URI, e);
    247             return null;
    248         }
    249         return inputFileDescriptor;
    250     }
    251 
    252     private static File copyDataToLocalFile(
    253             Context context, ParcelFileDescriptor inputFileDescriptor) {
    254 
    255         // Adopt the ParcelFileDescriptor into a try-with-resources so we will close it when we're
    256         // done regardless of the outcome.
    257         try (ParcelFileDescriptor pfd = inputFileDescriptor) {
    258             File localFile;
    259             try {
    260                 localFile = File.createTempFile("temp", ".zip", context.getFilesDir());
    261             } catch (IOException e) {
    262                 Log.e(TAG, "Unable to create local storage file", e);
    263                 return null;
    264             }
    265 
    266             InputStream fis = new FileInputStream(pfd.getFileDescriptor(), false /* isFdOwner */);
    267             try (FileOutputStream fos = new FileOutputStream(localFile, false /* append */)) {
    268                 Streams.copy(fis, fos);
    269             } catch (IOException e) {
    270                 Log.e(TAG, "Unable to create asset storage file: " + localFile, e);
    271                 return null;
    272             }
    273             return localFile;
    274         } catch (IOException e) {
    275             Log.e(TAG, "Unable to close ParcelFileDescriptor", e);
    276             return null;
    277         }
    278     }
    279 
    280     private void handleInstall(final byte[] checkToken, final File localFile) {
    281         // Create a ParcelFileDescriptor pointing to localFile.
    282         final ParcelFileDescriptor distroFileDescriptor;
    283         try {
    284             distroFileDescriptor =
    285                     ParcelFileDescriptor.open(localFile, ParcelFileDescriptor.MODE_READ_ONLY);
    286         } catch (FileNotFoundException e) {
    287             Log.e(TAG, "Unable to create ParcelFileDescriptor from " + localFile);
    288             handleCheckComplete(checkToken, false /* success */);
    289             return;
    290         } finally {
    291             // It is safe to delete the File at this point. The ParcelFileDescriptor has an open
    292             // file descriptor to it if we are successful, or it is not going to be used if we are
    293             // returning early.
    294             localFile.delete();
    295         }
    296 
    297         Callback callback = new Callback() {
    298             @Override
    299             public void onFinished(int status) {
    300                 Log.i(TAG, "Finished install: " + status);
    301             }
    302         };
    303 
    304         // Adopt the distroFileDescriptor here so the local file descriptor is closed, whatever the
    305         // outcome.
    306         try (ParcelFileDescriptor pfd = distroFileDescriptor) {
    307             String tokenString = Arrays.toString(checkToken);
    308             EventLogTags.writeTimezoneCheckRequestInstall(tokenString);
    309             int requestStatus = mRulesManager.requestInstall(pfd, checkToken, callback);
    310             Log.i(TAG, "requestInstall() called, token=" + tokenString
    311                     + ", returned " + requestStatus);
    312         } catch (Exception e) {
    313             Log.e(TAG, "Error calling requestInstall()", e);
    314         }
    315     }
    316 
    317     private void handleUninstall(byte[] checkToken) {
    318         Callback callback = new Callback() {
    319             @Override
    320             public void onFinished(int status) {
    321                 Log.i(TAG, "Finished uninstall: " + status);
    322             }
    323         };
    324 
    325         try {
    326             String tokenString = Arrays.toString(checkToken);
    327             EventLogTags.writeTimezoneCheckRequestUninstall(tokenString);
    328             int requestStatus = mRulesManager.requestUninstall(checkToken, callback);
    329             Log.i(TAG, "requestUninstall() called, token=" + tokenString
    330                     + ", returned " + requestStatus);
    331         } catch (Exception e) {
    332             Log.e(TAG, "Error calling requestUninstall()", e);
    333         }
    334     }
    335 
    336     private void handleCheckComplete(final byte[] token, final boolean success) {
    337         try {
    338             String tokenString = Arrays.toString(token);
    339             EventLogTags.writeTimezoneCheckRequestNothing(tokenString, success ? 1 : 0);
    340             mRulesManager.requestNothing(token, success);
    341             Log.i(TAG, "requestNothing() called, token=" + tokenString + ", success=" + success);
    342         } catch (Exception e) {
    343             Log.e(TAG, "Error calling requestNothing()", e);
    344         }
    345     }
    346 
    347     private static class DistroOperation {
    348         final String mType;
    349         final DistroFormatVersion mDistroFormatVersion;
    350         final DistroRulesVersion mDistroRulesVersion;
    351 
    352         DistroOperation(String type, DistroFormatVersion distroFormatVersion,
    353                 DistroRulesVersion distroRulesVersion) {
    354             mType = type;
    355             mDistroFormatVersion = distroFormatVersion;
    356             mDistroRulesVersion = distroRulesVersion;
    357         }
    358 
    359         @Override
    360         public String toString() {
    361             return "DistroOperation{" +
    362                     "mType='" + mType + '\'' +
    363                     ", mDistroFormatVersion=" + mDistroFormatVersion +
    364                     ", mDistroRulesVersion=" + mDistroRulesVersion +
    365                     '}';
    366         }
    367     }
    368 }
    369