Home | History | Annotate | Download | only in persistentlog
      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.dialer.persistentlog;
     18 
     19 import android.annotation.TargetApi;
     20 import android.content.Context;
     21 import android.content.SharedPreferences;
     22 import android.os.Build.VERSION_CODES;
     23 import android.preference.PreferenceManager;
     24 import android.support.annotation.AnyThread;
     25 import android.support.annotation.MainThread;
     26 import android.support.annotation.NonNull;
     27 import android.support.annotation.Nullable;
     28 import android.support.annotation.WorkerThread;
     29 import android.support.v4.os.UserManagerCompat;
     30 import java.io.ByteArrayInputStream;
     31 import java.io.DataInputStream;
     32 import java.io.DataOutputStream;
     33 import java.io.EOFException;
     34 import java.io.File;
     35 import java.io.FileOutputStream;
     36 import java.io.IOException;
     37 import java.io.RandomAccessFile;
     38 import java.nio.ByteBuffer;
     39 import java.util.ArrayList;
     40 import java.util.Arrays;
     41 import java.util.List;
     42 
     43 /**
     44  * Handles serialization of byte arrays and read/write them to multiple rotating files. If a logText
     45  * file exceeds {@code fileSizeLimit} after a write, a new file will be used. if the total number of
     46  * files exceeds {@code fileCountLimit} the oldest ones will be deleted. The logs are stored in the
     47  * cache but the file index is stored in the data (clearing data will also clear the cache). The
     48  * logs will be stored under /cache_dir/persistent_log/{@code subfolder}, so multiple independent
     49  * logs can be created.
     50  *
     51  * <p>This class is NOT thread safe. All methods expect the constructor must be called on the same
     52  * worker thread.
     53  */
     54 @SuppressWarnings("AndroidApiChecker") // lambdas
     55 @TargetApi(VERSION_CODES.M)
     56 final class PersistentLogFileHandler {
     57 
     58   private static final String LOG_DIRECTORY = "persistent_log";
     59   private static final String NEXT_FILE_INDEX_PREFIX = "persistent_long_next_file_index_";
     60 
     61   private File logDirectory;
     62   private final String subfolder;
     63   private final int fileSizeLimit;
     64   private final int fileCountLimit;
     65 
     66   private SharedPreferences sharedPreferences;
     67 
     68   private File outputFile;
     69   private Context context;
     70 
     71   @MainThread
     72   PersistentLogFileHandler(String subfolder, int fileSizeLimit, int fileCountLimit) {
     73     this.subfolder = subfolder;
     74     this.fileSizeLimit = fileSizeLimit;
     75     this.fileCountLimit = fileCountLimit;
     76   }
     77 
     78   /** Must be called right after the logger thread is created. */
     79   @WorkerThread
     80   void initialize(Context context) {
     81     this.context = context;
     82     logDirectory = new File(new File(context.getCacheDir(), LOG_DIRECTORY), subfolder);
     83     initializeSharedPreference(context);
     84   }
     85 
     86   @WorkerThread
     87   private boolean initializeSharedPreference(Context context) {
     88     if (sharedPreferences == null && UserManagerCompat.isUserUnlocked(context)) {
     89       sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
     90       return true;
     91     }
     92     return sharedPreferences != null;
     93   }
     94 
     95   /**
     96    * Write the list of byte arrays to the current log file, prefixing each entry with its' length. A
     97    * new file will only be selected when the batch is completed, so the resulting file might be
     98    * larger then {@code fileSizeLimit}
     99    */
    100   @WorkerThread
    101   void writeLogs(List<byte[]> logs) throws IOException {
    102     if (outputFile == null) {
    103       selectNextFileToWrite();
    104     }
    105     outputFile.createNewFile();
    106     try (DataOutputStream outputStream =
    107         new DataOutputStream(new FileOutputStream(outputFile, true))) {
    108       for (byte[] log : logs) {
    109         outputStream.writeInt(log.length);
    110         outputStream.write(log);
    111       }
    112       outputStream.close();
    113       if (outputFile.length() > fileSizeLimit) {
    114         selectNextFileToWrite();
    115       }
    116     }
    117   }
    118 
    119   /** Concatenate all log files in chronicle order and return a byte array. */
    120   @WorkerThread
    121   @NonNull
    122   private byte[] readBlob() throws IOException {
    123     File[] files = getLogFiles();
    124 
    125     ByteBuffer byteBuffer = ByteBuffer.allocate(getTotalSize(files));
    126     for (File file : files) {
    127       byteBuffer.put(readAllBytes(file));
    128     }
    129     return byteBuffer.array();
    130   }
    131 
    132   private static int getTotalSize(File[] files) {
    133     int sum = 0;
    134     for (File file : files) {
    135       sum += (int) file.length();
    136     }
    137     return sum;
    138   }
    139 
    140   /** Parses the content of all files back to individual byte arrays. */
    141   @WorkerThread
    142   @NonNull
    143   List<byte[]> getLogs() throws IOException {
    144     byte[] blob = readBlob();
    145     List<byte[]> logs = new ArrayList<>();
    146     try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(blob))) {
    147       byte[] log = readLog(input);
    148       while (log != null) {
    149         logs.add(log);
    150         log = readLog(input);
    151       }
    152     }
    153     return logs;
    154   }
    155 
    156   @WorkerThread
    157   private void selectNextFileToWrite() throws IOException {
    158     File[] files = getLogFiles();
    159 
    160     if (files.length == 0 || files[files.length - 1].length() > fileSizeLimit) {
    161       if (files.length >= fileCountLimit) {
    162         for (int i = 0; i <= files.length - fileCountLimit; i++) {
    163           files[i].delete();
    164         }
    165       }
    166       outputFile = new File(logDirectory, String.valueOf(getAndIncrementNextFileIndex()));
    167     } else {
    168       outputFile = files[files.length - 1];
    169     }
    170   }
    171 
    172   @NonNull
    173   @WorkerThread
    174   private File[] getLogFiles() {
    175     logDirectory.mkdirs();
    176     File[] files = logDirectory.listFiles();
    177     if (files == null) {
    178       files = new File[0];
    179     }
    180     Arrays.sort(
    181         files,
    182         (File lhs, File rhs) ->
    183             Long.compare(Long.valueOf(lhs.getName()), Long.valueOf(rhs.getName())));
    184     return files;
    185   }
    186 
    187   @Nullable
    188   @WorkerThread
    189   private static byte[] readLog(DataInputStream inputStream) throws IOException {
    190     try {
    191       byte[] data = new byte[inputStream.readInt()];
    192       inputStream.read(data);
    193       return data;
    194     } catch (EOFException e) {
    195       return null;
    196     }
    197   }
    198 
    199   @NonNull
    200   @WorkerThread
    201   private static byte[] readAllBytes(File file) throws IOException {
    202     byte[] result = new byte[(int) file.length()];
    203     try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
    204       randomAccessFile.readFully(result);
    205     }
    206     return result;
    207   }
    208 
    209   @WorkerThread
    210   private int getAndIncrementNextFileIndex() throws IOException {
    211     if (!initializeSharedPreference(context)) {
    212       throw new IOException("Shared preference is not available");
    213     }
    214 
    215     int index = sharedPreferences.getInt(getNextFileKey(), 0);
    216     sharedPreferences.edit().putInt(getNextFileKey(), index + 1).commit();
    217     return index;
    218   }
    219 
    220   @AnyThread
    221   private String getNextFileKey() {
    222     return NEXT_FILE_INDEX_PREFIX + subfolder;
    223   }
    224 }
    225