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 17 package com.android.server.backup; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.util.Slog; 22 23 import java.io.BufferedInputStream; 24 import java.io.DataInputStream; 25 import java.io.EOFException; 26 import java.io.File; 27 import java.io.FileInputStream; 28 import java.io.IOException; 29 import java.io.InputStream; 30 import java.io.RandomAccessFile; 31 import java.util.ArrayList; 32 import java.util.List; 33 import java.util.Objects; 34 import java.util.function.Consumer; 35 36 /** 37 * A journal of packages that have indicated that their data has changed (and therefore should be 38 * backed up in the next scheduled K/V backup pass). 39 * 40 * <p>This information is persisted to the filesystem so that it is not lost in the event of a 41 * reboot. 42 */ 43 public class DataChangedJournal { 44 private static final String TAG = "DataChangedJournal"; 45 private static final String FILE_NAME_PREFIX = "journal"; 46 47 /** 48 * Journals tend to be on the order of a few kilobytes, hence setting the buffer size to 8kb. 49 */ 50 private static final int BUFFER_SIZE_BYTES = 8 * 1024; 51 52 private final File mFile; 53 54 /** 55 * Constructs an instance that reads from and writes to the given file. 56 */ DataChangedJournal(@onNull File file)57 DataChangedJournal(@NonNull File file) { 58 mFile = Objects.requireNonNull(file); 59 } 60 61 62 /** 63 * Adds the given package to the journal. 64 * 65 * @param packageName The name of the package whose data has changed. 66 * @throws IOException if there is an IO error writing to the journal file. 67 */ addPackage(String packageName)68 public void addPackage(String packageName) throws IOException { 69 try (RandomAccessFile out = new RandomAccessFile(mFile, "rws")) { 70 out.seek(out.length()); 71 out.writeUTF(packageName); 72 } 73 } 74 75 /** 76 * Invokes {@link Consumer#accept(Object)} with every package name in the journal file. 77 * 78 * @param consumer The callback. 79 * @throws IOException If there is an IO error reading from the file. 80 */ forEach(Consumer<String> consumer)81 public void forEach(Consumer<String> consumer) throws IOException { 82 try ( 83 InputStream in = new FileInputStream(mFile); 84 InputStream bufferedIn = new BufferedInputStream(in, BUFFER_SIZE_BYTES); 85 DataInputStream dataInputStream = new DataInputStream(bufferedIn) 86 ) { 87 while (true) { 88 String packageName = dataInputStream.readUTF(); 89 consumer.accept(packageName); 90 } 91 } catch (EOFException tolerated) { 92 // no more data; we're done 93 } // other kinds of IOExceptions are error conditions and handled in the caller 94 } 95 96 /** 97 * Returns a list with the packages in this journal. 98 * 99 * @throws IOException If there is an IO error reading from the file. 100 */ getPackages()101 public List<String> getPackages() throws IOException { 102 List<String> packages = new ArrayList<>(); 103 forEach(packages::add); 104 return packages; 105 } 106 107 /** 108 * Deletes the journal from the filesystem. 109 * 110 * @return {@code true} if successfully deleted journal. 111 */ delete()112 public boolean delete() { 113 return mFile.delete(); 114 } 115 116 @Override hashCode()117 public int hashCode() { 118 return mFile.hashCode(); 119 } 120 121 @Override equals(@ullable Object object)122 public boolean equals(@Nullable Object object) { 123 if (object instanceof DataChangedJournal) { 124 DataChangedJournal that = (DataChangedJournal) object; 125 return mFile.equals(that.mFile); 126 } 127 return false; 128 } 129 130 @Override toString()131 public String toString() { 132 return mFile.toString(); 133 } 134 135 /** 136 * Creates a new journal with a random file name in the given journal directory. 137 * 138 * @param journalDirectory The directory where journals are kept. 139 * @return The journal. 140 * @throws IOException if there is an IO error creating the file. 141 */ newJournal(@onNull File journalDirectory)142 static DataChangedJournal newJournal(@NonNull File journalDirectory) throws IOException { 143 Objects.requireNonNull(journalDirectory); 144 File file = File.createTempFile(FILE_NAME_PREFIX, null, journalDirectory); 145 return new DataChangedJournal(file); 146 } 147 148 /** 149 * Returns a list of journals in the given journal directory. 150 */ listJournals(File journalDirectory)151 static ArrayList<DataChangedJournal> listJournals(File journalDirectory) { 152 ArrayList<DataChangedJournal> journals = new ArrayList<>(); 153 File[] journalFiles = journalDirectory.listFiles(); 154 if (journalFiles == null) { 155 Slog.w(TAG, "Failed to read journal files"); 156 return journals; 157 } 158 for (File file : journalFiles) { 159 journals.add(new DataChangedJournal(file)); 160 } 161 return journals; 162 } 163 } 164