ConfigUpdateInstallReceiver.java revision 12964bf5dd36d13fc25efef7c7175a677f9490a5
1/* 2 * Copyright (C) 2012 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 17package com.android.server.updates; 18 19import com.android.server.EventLogTags; 20 21import android.content.BroadcastReceiver; 22import android.content.Context; 23import android.content.Intent; 24import android.net.Uri; 25import android.util.EventLog; 26import android.util.Slog; 27 28import java.io.File; 29import java.io.FileOutputStream; 30import java.io.IOException; 31import java.io.InputStream; 32import java.security.MessageDigest; 33import java.security.NoSuchAlgorithmException; 34 35import libcore.io.IoUtils; 36import libcore.io.Streams; 37 38public class ConfigUpdateInstallReceiver extends BroadcastReceiver { 39 40 private static final String TAG = "ConfigUpdateInstallReceiver"; 41 42 private static final String EXTRA_REQUIRED_HASH = "REQUIRED_HASH"; 43 private static final String EXTRA_VERSION_NUMBER = "VERSION"; 44 45 protected final File updateDir; 46 protected final File updateContent; 47 protected final File updateVersion; 48 49 public ConfigUpdateInstallReceiver(String updateDir, String updateContentPath, 50 String updateMetadataPath, String updateVersionPath) { 51 this.updateDir = new File(updateDir); 52 this.updateContent = new File(updateDir, updateContentPath); 53 File updateMetadataDir = new File(updateDir, updateMetadataPath); 54 this.updateVersion = new File(updateMetadataDir, updateVersionPath); 55 } 56 57 @Override 58 public void onReceive(final Context context, final Intent intent) { 59 new Thread() { 60 @Override 61 public void run() { 62 try { 63 // get the content path from the extras 64 byte[] altContent = getAltContent(context, intent); 65 // get the version from the extras 66 int altVersion = getVersionFromIntent(intent); 67 // get the previous value from the extras 68 String altRequiredHash = getRequiredHashFromIntent(intent); 69 // get the version currently being used 70 int currentVersion = getCurrentVersion(); 71 // get the hash of the currently used value 72 String currentHash = getCurrentHash(getCurrentContent()); 73 if (!verifyVersion(currentVersion, altVersion)) { 74 Slog.i(TAG, "Not installing, new version is <= current version"); 75 } else if (!verifyPreviousHash(currentHash, altRequiredHash)) { 76 EventLog.writeEvent(EventLogTags.CONFIG_INSTALL_FAILED, 77 "Current hash did not match required value"); 78 } else { 79 // install the new content 80 Slog.i(TAG, "Found new update, installing..."); 81 install(altContent, altVersion); 82 Slog.i(TAG, "Installation successful"); 83 postInstall(context, intent); 84 } 85 } catch (Exception e) { 86 Slog.e(TAG, "Could not update content!", e); 87 // keep the error message <= 100 chars 88 String errMsg = e.toString(); 89 if (errMsg.length() > 100) { 90 errMsg = errMsg.substring(0, 99); 91 } 92 EventLog.writeEvent(EventLogTags.CONFIG_INSTALL_FAILED, errMsg); 93 } 94 } 95 }.start(); 96 } 97 98 private Uri getContentFromIntent(Intent i) { 99 Uri data = i.getData(); 100 if (data == null) { 101 throw new IllegalStateException("Missing required content path, ignoring."); 102 } 103 return data; 104 } 105 106 private int getVersionFromIntent(Intent i) throws NumberFormatException { 107 String extraValue = i.getStringExtra(EXTRA_VERSION_NUMBER); 108 if (extraValue == null) { 109 throw new IllegalStateException("Missing required version number, ignoring."); 110 } 111 return Integer.parseInt(extraValue.trim()); 112 } 113 114 private String getRequiredHashFromIntent(Intent i) { 115 String extraValue = i.getStringExtra(EXTRA_REQUIRED_HASH); 116 if (extraValue == null) { 117 throw new IllegalStateException("Missing required previous hash, ignoring."); 118 } 119 return extraValue.trim(); 120 } 121 122 private int getCurrentVersion() throws NumberFormatException { 123 try { 124 String strVersion = IoUtils.readFileAsString(updateVersion.getCanonicalPath()).trim(); 125 return Integer.parseInt(strVersion); 126 } catch (IOException e) { 127 Slog.i(TAG, "Couldn't find current metadata, assuming first update"); 128 return 0; 129 } 130 } 131 132 private byte[] getAltContent(Context c, Intent i) throws IOException { 133 Uri content = getContentFromIntent(i); 134 InputStream is = c.getContentResolver().openInputStream(content); 135 try { 136 return Streams.readFullyNoClose(is); 137 } finally { 138 is.close(); 139 } 140 } 141 142 private byte[] getCurrentContent() { 143 try { 144 return IoUtils.readFileAsByteArray(updateContent.getCanonicalPath()); 145 } catch (IOException e) { 146 Slog.i(TAG, "Failed to read current content, assuming first update!"); 147 return null; 148 } 149 } 150 151 private static String getCurrentHash(byte[] content) { 152 if (content == null) { 153 return "0"; 154 } 155 try { 156 MessageDigest dgst = MessageDigest.getInstance("SHA512"); 157 byte[] fingerprint = dgst.digest(content); 158 return IntegralToString.bytesToHexString(fingerprint, false); 159 } catch (NoSuchAlgorithmException e) { 160 throw new AssertionError(e); 161 } 162 } 163 164 private boolean verifyVersion(int current, int alternative) { 165 return (current < alternative); 166 } 167 168 private boolean verifyPreviousHash(String current, String required) { 169 // this is an optional value- if the required field is NONE then we ignore it 170 if (required.equals("NONE")) { 171 return true; 172 } 173 // otherwise, verify that we match correctly 174 return current.equals(required); 175 } 176 177 protected void writeUpdate(File dir, File file, byte[] content) throws IOException { 178 FileOutputStream out = null; 179 File tmp = null; 180 try { 181 // create the parents for the destination file 182 File parent = file.getParentFile(); 183 parent.mkdirs(); 184 // check that they were created correctly 185 if (!parent.exists()) { 186 throw new IOException("Failed to create directory " + parent.getCanonicalPath()); 187 } 188 // create the temporary file 189 tmp = File.createTempFile("journal", "", dir); 190 // mark tmp -rw-r--r-- 191 tmp.setReadable(true, false); 192 // write to it 193 out = new FileOutputStream(tmp); 194 out.write(content); 195 // sync to disk 196 out.getFD().sync(); 197 // atomic rename 198 if (!tmp.renameTo(file)) { 199 throw new IOException("Failed to atomically rename " + file.getCanonicalPath()); 200 } 201 } finally { 202 if (tmp != null) { 203 tmp.delete(); 204 } 205 IoUtils.closeQuietly(out); 206 } 207 } 208 209 protected void install(byte[] content, int version) throws IOException { 210 writeUpdate(updateDir, updateContent, content); 211 writeUpdate(updateDir, updateVersion, Long.toString(version).getBytes()); 212 } 213 214 protected void postInstall(Context context, Intent intent) { 215 } 216} 217