• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.providers.telephony;
18 
19 import static java.util.Arrays.stream;
20 
21 import android.annotation.NonNull;
22 import android.content.Context;
23 import android.database.Cursor;
24 import android.database.sqlite.SQLiteDatabase;
25 import android.os.Bundle;
26 import android.os.FileUtils;
27 import android.provider.Telephony;
28 import android.util.Log;
29 
30 import java.io.File;
31 import java.io.IOException;
32 import java.util.Arrays;
33 import java.util.HashSet;
34 import java.util.Set;
35 import java.util.stream.Collectors;
36 
37 /** Tools to find and delete unreferenced MMS parts from the parts directory. */
38 public class MmsPartsCleanup {
39     private static final String TAG = "MmsPartsCleanup";
40 
41     static final String PART_FILE_COUNT = "part_file_count";
42     static final String PART_TABLE_ENTRY_COUNT = "part_table_entry_count";
43     static final String DELETED_COUNT = "deleted_count";
44 
45     /**
46      * @param context
47      * @param doDelete if true, delete the unreferenced MMS part files found in the data dir.
48      *                 if false, compute the number of unreferenced part files and return the
49      *                 results, but don't actually delete the files
50      * @param bundle an existing bundle where int values will be added:
51      *               PART_FILE_COUNT - an int count of MMS attachment files in the parts
52      *                                 directory
53      *               PART_TABLE_ENTRY_COUNT - an int count of MMS attachments referenced
54      *                                        by existing MMS messages
55      *               DELETED_COUNT - the number of non-referenced MMS part files delete (or would
56      *                               be deleted if doDelete is true)
57      */
cleanupDanglingParts(Context context, boolean doDelete, Bundle bundle)58     public static void cleanupDanglingParts(Context context, boolean doDelete, Bundle bundle) {
59         Set<String> danglingFilePathsToDelete = getDanglingMmsParts(context, bundle);
60         danglingFilePathsToDelete.forEach(path -> {
61             File partFile = new File(path);
62             if (partFile.exists()) {
63                 if (doDelete) {
64                     Log.d(TAG, "Deleting dangling MMS part: " + partFile.getAbsolutePath());
65                     partFile.delete();
66                 } else {
67                     Log.d(TAG,
68                             "Would have deleted dangling MMS part: "
69                                     + partFile.getAbsolutePath());
70                 }
71             } else {
72                 Log.wtf(TAG,
73                         "Part file path does not exist: "
74                                 + partFile.getAbsolutePath());
75             }
76         });
77         bundle.putInt(DELETED_COUNT, danglingFilePathsToDelete.size());
78     }
79 
80     /**
81      * @param context
82      * @return a set of file path names for every MMS attachment file found in the data directory
83      */
84     @NonNull
getAllMmsPartsInPartsDir(Context context)85     private static Set<String> getAllMmsPartsInPartsDir(Context context) {
86         Set<String> allMmsAttachments = new HashSet<>();
87         try {
88             String partsDirPath = context.getDir(MmsProvider.PARTS_DIR_NAME, 0)
89                     .getCanonicalPath();
90             Log.d(TAG, "getDanglingMmsParts: " + partsDirPath);
91             File partsDir = new File(partsDirPath);
92             allMmsAttachments = Arrays.stream(FileUtils.listFilesOrEmpty(partsDir)).map(p ->
93             {
94                 try {
95                     return p.getCanonicalPath();
96                 } catch (IOException e) {
97                     Log.d(TAG, "getDanglingMmsParts: couldn't get path for " + p);
98                 }
99                 return null;
100             }).filter(path -> path != null).collect(Collectors.toSet());
101         } catch (IOException e) {
102             Log.e(TAG, "getDanglingMmsParts: failed " + e, e);
103             allMmsAttachments.clear();
104         }
105         return allMmsAttachments;
106     }
107 
108     /**
109      * @param partFilePaths the incoming set contains the file paths of all MMS attachments in the
110      *                      data directory. This function modifies this set by removing any path
111      *                      that is referenced from an MMS part in the parts table. In other words,
112      *                      when this function returns, partFilePaths contains only the unreferenced
113      *                      file paths
114      * @param context
115      * @return the number of MMS parts that are referenced and still attached to an MMS message
116      */
117     @NonNull
removeReferencedPartsFromSet(Set<String> partFilePaths, Context context)118     private static int removeReferencedPartsFromSet(Set<String> partFilePaths, Context context) {
119         if (partFilePaths.isEmpty()) {
120             return 0;
121         }
122         SQLiteDatabase db = MmsSmsDatabaseHelper.getInstanceForCe(context).getReadableDatabase();
123         int referencedPartCount = 0;
124         try (Cursor partRows =
125                      db.query(
126                              MmsProvider.TABLE_PART,
127                              new String[] {Telephony.Mms.Part._DATA},
128                              Telephony.Mms.Part._DATA + " IS NOT NULL AND "
129                                      + Telephony.Mms.Part._DATA + " <> ''",
130                              null,
131                              null,
132                              null,
133                              null)) {
134             if (partRows != null) {
135                 while (partRows.moveToNext()) {
136                     String path = partRows.getString(0);
137                     if (path != null) {
138                         if (partFilePaths.remove(path)) {
139                             ++referencedPartCount;
140                         }
141                     }
142                 }
143             }
144         }
145         return referencedPartCount;
146     }
147 
148     /**
149      * @param context
150      * @param bundle an existing bundle that is modified to contain two possible key/values:
151      *               PART_FILE_COUNT - an int count of MMS attachment files in the parts directory
152      *               PART_TABLE_ENTRY_COUNT - an int count of MMS attachments referenced by existing
153      *                                        MMS messages
154      * @return a set of file path names for MMS attachments that live in the data directory, but
155      *         aren't referenced by any existing MMS message
156      */
157     @NonNull
getDanglingMmsParts(Context context, Bundle bundle)158     private static Set<String> getDanglingMmsParts(Context context, Bundle bundle) {
159         Set<String> allExistingMmsAttachments = getAllMmsPartsInPartsDir(context);
160         bundle.putInt(PART_FILE_COUNT, allExistingMmsAttachments.size());
161         bundle.putInt(PART_TABLE_ENTRY_COUNT,
162                 removeReferencedPartsFromSet(allExistingMmsAttachments, context));
163 
164         return allExistingMmsAttachments;
165     }
166 }
167