Skip to content

Commit c75caee

Browse files
authored
Delete custom models from device (#2236)
* Reviewer suggested changes. * Add code for custom model deletion from a device. * Add code for custom model deletion from a device. * Update getFile so it checks if the file exists before returning. * Adding logging todo. * Adding logging todo.
1 parent 13b6736 commit c75caee

File tree

10 files changed

+229
-46
lines changed

10 files changed

+229
-46
lines changed

firebase-ml-modeldownloader/api.txt

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,3 @@ package com.google.firebase.ml.modeldownloader {
3939

4040
}
4141

42-
package com.google.firebase.ml.modeldownloader.internal {
43-
44-
public class ModelFileManager {
45-
ctor public ModelFileManager(@NonNull com.google.firebase.FirebaseApp);
46-
method @NonNull public static com.google.firebase.ml.modeldownloader.internal.ModelFileManager getInstance();
47-
method @Nullable @WorkerThread public java.io.File moveModelToDestinationFolder(@NonNull com.google.firebase.ml.modeldownloader.CustomModel, @NonNull android.os.ParcelFileDescriptor) throws java.lang.Exception;
48-
}
49-
50-
}
51-

firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/CustomModel.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,12 @@ File getFile(ModelFileDownloadService fileDownloadService) throws Exception {
152152
if (localFilePath == null || localFilePath.isEmpty()) {
153153
return null;
154154
}
155-
return new File(localFilePath);
155+
File modelFile = new File(localFilePath);
156+
157+
if (!modelFile.exists()) {
158+
return null;
159+
}
160+
return modelFile;
156161
}
157162

158163
/**

firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/FirebaseModelDownloader.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.google.firebase.installations.FirebaseInstallationsApi;
2828
import com.google.firebase.ml.modeldownloader.internal.CustomModelDownloadService;
2929
import com.google.firebase.ml.modeldownloader.internal.ModelFileDownloadService;
30+
import com.google.firebase.ml.modeldownloader.internal.ModelFileManager;
3031
import com.google.firebase.ml.modeldownloader.internal.SharedPreferencesUtil;
3132
import java.util.Set;
3233
import java.util.concurrent.Executor;
@@ -37,6 +38,7 @@ public class FirebaseModelDownloader {
3738
private final FirebaseOptions firebaseOptions;
3839
private final SharedPreferencesUtil sharedPreferencesUtil;
3940
private final ModelFileDownloadService fileDownloadService;
41+
private final ModelFileManager fileManager;
4042
private final CustomModelDownloadService modelDownloadService;
4143
private final Executor executor;
4244

@@ -48,7 +50,8 @@ public class FirebaseModelDownloader {
4850
this.sharedPreferencesUtil = new SharedPreferencesUtil(firebaseApp);
4951
this.modelDownloadService =
5052
new CustomModelDownloadService(firebaseOptions, firebaseInstallationsApi);
51-
this.executor = Executors.newCachedThreadPool();
53+
this.executor = Executors.newSingleThreadExecutor();
54+
fileManager = ModelFileManager.getInstance();
5255
}
5356

5457
@VisibleForTesting
@@ -57,11 +60,13 @@ public class FirebaseModelDownloader {
5760
SharedPreferencesUtil sharedPreferencesUtil,
5861
ModelFileDownloadService fileDownloadService,
5962
CustomModelDownloadService modelDownloadService,
63+
ModelFileManager fileManager,
6064
Executor executor) {
6165
this.firebaseOptions = firebaseOptions;
6266
this.sharedPreferencesUtil = sharedPreferencesUtil;
6367
this.fileDownloadService = fileDownloadService;
6468
this.modelDownloadService = modelDownloadService;
69+
this.fileManager = fileManager;
6570
this.executor = executor;
6671
}
6772

@@ -187,7 +192,16 @@ public Task<Set<CustomModel>> listDownloadedModels() {
187192
*/
188193
@NonNull
189194
public Task<Void> deleteDownloadedModel(@NonNull String modelName) {
190-
throw new UnsupportedOperationException("Not yet implemented.");
195+
196+
TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();
197+
executor.execute(
198+
() -> {
199+
// remove all files associated with this model and then clean up model references.
200+
fileManager.deleteAllModels(modelName);
201+
sharedPreferencesUtil.clearModelDetails(modelName);
202+
taskCompletionSource.setResult(null);
203+
});
204+
return taskCompletionSource.getTask();
191205
}
192206

193207
/** Returns the nick name of the {@link FirebaseApp} of this {@link FirebaseModelDownloader} */

firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/ModelFileManager.java

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,28 @@
1818
import android.os.Build.VERSION_CODES;
1919
import android.os.ParcelFileDescriptor;
2020
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
21+
import android.util.Log;
2122
import androidx.annotation.NonNull;
2223
import androidx.annotation.Nullable;
2324
import androidx.annotation.VisibleForTesting;
2425
import androidx.annotation.WorkerThread;
26+
import com.google.android.gms.common.internal.Preconditions;
2527
import com.google.firebase.FirebaseApp;
2628
import com.google.firebase.ml.modeldownloader.CustomModel;
2729
import java.io.File;
2830
import java.io.FileInputStream;
2931
import java.io.FileOutputStream;
3032
import java.io.IOException;
3133

32-
/** Model File Manager is used to move the downloaded file to the appropriate locations. */
34+
/**
35+
* Model File Manager is used to move the downloaded file to the appropriate locations.
36+
*
37+
* @hide
38+
*/
3339
public class ModelFileManager {
3440

41+
private static final String TAG = "FirebaseModelFileManage";
42+
3543
@VisibleForTesting
3644
static final String CUSTOM_MODEL_ROOT_PATH = "com.google.firebase.ml.custom.models";
3745

@@ -108,7 +116,7 @@ private int getLatestCachedModelVersion(@NonNull File modelDir) {
108116
try {
109117
index = Math.max(index, Integer.parseInt(modelFile.getName()));
110118
} catch (NumberFormatException e) {
111-
System.out.println("Contains non-integer file name " + modelFile.getName());
119+
Log.d(TAG, String.format("Contains non-integer file name %s", modelFile.getName()));
112120
}
113121
}
114122
return index;
@@ -118,6 +126,19 @@ private int getLatestCachedModelVersion(@NonNull File modelDir) {
118126
@Nullable
119127
File getModelFileDestination(@NonNull CustomModel model) throws Exception {
120128
File destFolder = getDirImpl(model.getName());
129+
if (!destFolder.exists()) {
130+
Log.d(TAG, "model folder does not exist, creating one: " + destFolder.getAbsolutePath());
131+
if (!destFolder.mkdirs()) {
132+
// TODO(annzimmer) change to FirebaseMLException when logging complete. Add test at same
133+
// time.
134+
throw new Exception("Failed to create model folder: " + destFolder);
135+
}
136+
} else if (!destFolder.isDirectory()) {
137+
// TODO(annzimmer) change to FirebaseMLException when logging complete. Add test at same time.
138+
throw new Exception(
139+
"Can not create model folder, since an existing file has the same name: " + destFolder);
140+
}
141+
121142
int index = getLatestCachedModelVersion(destFolder);
122143
return new File(destFolder, String.valueOf(index + 1));
123144
}
@@ -141,6 +162,11 @@ public synchronized File moveModelToDestinationFolder(
141162
@NonNull CustomModel customModel, @NonNull ParcelFileDescriptor modelFileDescriptor)
142163
throws Exception {
143164
File modelFileDestination = getModelFileDestination(customModel);
165+
// why would this ever be true?
166+
File modelFolder = modelFileDestination.getParentFile();
167+
if (!modelFolder.exists()) {
168+
modelFolder.mkdirs();
169+
}
144170

145171
// Moves to the final destination file in app private folder to avoid the downloaded file from
146172
// being changed by
@@ -156,11 +182,44 @@ public synchronized File moveModelToDestinationFolder(
156182
fos.getFD().sync();
157183
} catch (IOException e) {
158184
// Failed to copy to destination - clean up.
159-
System.out.println("Failed to copy downloaded model file to destination folder: " + e);
185+
Log.d(TAG, "Failed to copy downloaded model file to destination folder: " + e.toString());
160186
modelFileDestination.delete();
161187
return null;
162188
}
163189

164190
return modelFileDestination;
165191
}
192+
193+
/**
194+
* Deletes all previously cached Model File(s) and the model root folder.
195+
*
196+
* <p>All model and model support files are stored in temp folder until the model gets fully
197+
* downloaded, validated and hash-checked. We delete both temp files and files in the final
198+
* destination for a model in this method.
199+
*/
200+
@WorkerThread
201+
public synchronized void deleteAllModels(@NonNull String modelName) {
202+
File modelFolder = getModelDirUnsafe(modelName);
203+
deleteRecursively(modelFolder);
204+
}
205+
206+
/**
207+
* Deletes all files under the {@code root}. If @{code root} is a file, just itself will be
208+
* deleted. If it is a folder, all files and subfolders will be deleted, including {@code root}
209+
* itself.
210+
*/
211+
boolean deleteRecursively(@Nullable File root) {
212+
if (root == null) {
213+
return false;
214+
}
215+
216+
boolean ret = true;
217+
if (root.isDirectory()) {
218+
for (File f : Preconditions.checkNotNull(root.listFiles())) {
219+
ret = ret && deleteRecursively(f);
220+
}
221+
}
222+
223+
return ret && root.delete();
224+
}
166225
}

firebase-ml-modeldownloader/src/main/java/com/google/firebase/ml/modeldownloader/internal/SharedPreferencesUtil.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,12 @@ public synchronized void setFailedUploadedCustomModelDetails(@NonNull String cus
193193
/**
194194
* Clears all stored data related to a local custom model, including download details.
195195
*
196+
* <p>Call ModelFileManager.deleteAllModels() before calling this to trigger successful clean up
197+
* of downloaded files.
198+
*
196199
* @param modelName - name of model
197200
*/
198-
public synchronized void clearModelDetails(@NonNull String modelName, boolean cleanUpModelFile) {
199-
if (cleanUpModelFile) {
200-
// TODO(annz) - add code to remove model files from device
201-
}
201+
public synchronized void clearModelDetails(@NonNull String modelName) {
202202
Editor editor = getSharedPreferences().edit();
203203

204204
clearDownloadingModelDetails(editor, modelName);

firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/CustomModelTest.java

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import static org.junit.Assert.assertEquals;
1818
import static org.junit.Assert.assertNotEquals;
1919
import static org.junit.Assert.assertNull;
20+
import static org.junit.Assert.assertTrue;
2021
import static org.mockito.ArgumentMatchers.any;
2122
import static org.mockito.Mockito.times;
2223
import static org.mockito.Mockito.verify;
@@ -28,6 +29,8 @@
2829
import com.google.firebase.FirebaseOptions.Builder;
2930
import com.google.firebase.ml.modeldownloader.internal.ModelFileDownloadService;
3031
import java.io.File;
32+
import java.io.IOException;
33+
import org.junit.After;
3134
import org.junit.Before;
3235
import org.junit.Test;
3336
import org.junit.runner.RunWith;
@@ -37,33 +40,63 @@
3740

3841
@RunWith(RobolectricTestRunner.class)
3942
public class CustomModelTest {
43+
private static final String MODEL_NAME = "ModelName";
44+
private static final String MODEL_HASH = "dsf324";
4045

41-
public static final String MODEL_NAME = "ModelName";
42-
public static final String MODEL_HASH = "dsf324";
43-
public static final File TEST_MODEL_FILE = new File("fakeFile.tflite");
44-
public static final File TEST_MODEL_FILE_UPDATED = new File("fakeUpdateFile.tflite");
45-
46-
public static final String MODEL_URL = "https://project.firebase.com/modelName/23424.jpg";
47-
public static final String TEST_PROJECT_ID = "777777777777";
48-
public static final FirebaseOptions FIREBASE_OPTIONS =
46+
private static final String MODEL_URL = "https://project.firebase.com/modelName/23424.jpg";
47+
private static final String TEST_PROJECT_ID = "777777777777";
48+
private static final FirebaseOptions FIREBASE_OPTIONS =
4949
new Builder()
5050
.setApplicationId("1:123456789:android:abcdef")
5151
.setProjectId(TEST_PROJECT_ID)
5252
.build();
5353
private static final long URL_EXPIRATION = 604800L;
54-
final CustomModel CUSTOM_MODEL = new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0);
55-
final CustomModel CUSTOM_MODEL_URL =
54+
55+
private final CustomModel CUSTOM_MODEL = new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0);
56+
private final CustomModel CUSTOM_MODEL_URL =
5657
new CustomModel(MODEL_NAME, MODEL_HASH, 100, MODEL_URL, URL_EXPIRATION);
57-
final CustomModel CUSTOM_MODEL_FILE =
58-
new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0, TEST_MODEL_FILE.getPath());
59-
@Mock ModelFileDownloadService fileDownloadService;
58+
private final CustomModel CUSTOM_MODEL_BADFILE =
59+
new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0, "tmp/some/bad/filepath/model.tflite");
60+
61+
private File testModelFile;
62+
private File testModelFile2;
63+
private CustomModel customModelWithFile;
64+
65+
@Mock private ModelFileDownloadService fileDownloadService;
6066

6167
@Before
62-
public void setUp() {
68+
public void setUp() throws IOException {
6369
MockitoAnnotations.initMocks(this);
6470
FirebaseApp.clearInstancesForTest();
6571
// default app
66-
FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext(), FIREBASE_OPTIONS);
72+
FirebaseApp app =
73+
FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext(), FIREBASE_OPTIONS);
74+
setUpTestingFiles(app);
75+
customModelWithFile = new CustomModel(MODEL_NAME, MODEL_HASH, 100, 0, testModelFile.getPath());
76+
}
77+
78+
private void setUpTestingFiles(FirebaseApp app) throws IOException {
79+
final File testDir = new File(app.getApplicationContext().getNoBackupFilesDir(), "tmpModels");
80+
testDir.mkdirs();
81+
// make sure the directory is empty. Doesn't recurse into subdirs, but that's OK since
82+
// we're only using this directory for this test and we won't create any subdirs.
83+
for (File f : testDir.listFiles()) {
84+
if (f.isFile()) {
85+
f.delete();
86+
}
87+
}
88+
89+
testModelFile = File.createTempFile("tmpModelFile", "tflite");
90+
testModelFile2 = File.createTempFile("tmpModelFile2", "tflite");
91+
92+
assertTrue(testModelFile.exists());
93+
assertTrue(testModelFile2.exists());
94+
}
95+
96+
@After
97+
public void teardown() {
98+
testModelFile.deleteOnExit();
99+
testModelFile2.deleteOnExit();
67100
}
68101

69102
@Test
@@ -96,22 +129,29 @@ public void customModel_getFile_noLocalNoDownloadIncomplete() throws Exception {
96129
@Test
97130
public void customModel_getFile_localModelNoDownload() throws Exception {
98131
when(fileDownloadService.loadNewlyDownloadedModelFile(any(CustomModel.class))).thenReturn(null);
99-
assertEquals(CUSTOM_MODEL_FILE.getFile(fileDownloadService), TEST_MODEL_FILE);
132+
assertEquals(customModelWithFile.getFile(fileDownloadService), testModelFile);
133+
verify(fileDownloadService, times(1)).loadNewlyDownloadedModelFile(any());
134+
}
135+
136+
@Test
137+
public void customModel_getFile_localModelNoDownload_BadFile() throws Exception {
138+
when(fileDownloadService.loadNewlyDownloadedModelFile(any(CustomModel.class))).thenReturn(null);
139+
assertNull(CUSTOM_MODEL_BADFILE.getFile(fileDownloadService));
100140
verify(fileDownloadService, times(1)).loadNewlyDownloadedModelFile(any());
101141
}
102142

103143
@Test
104144
public void customModel_getFile_localModelDownloadComplete() throws Exception {
105145
when(fileDownloadService.loadNewlyDownloadedModelFile(any(CustomModel.class)))
106-
.thenReturn(TEST_MODEL_FILE_UPDATED);
107-
assertEquals(CUSTOM_MODEL_FILE.getFile(fileDownloadService), TEST_MODEL_FILE_UPDATED);
146+
.thenReturn(testModelFile2);
147+
assertEquals(customModelWithFile.getFile(fileDownloadService), testModelFile2);
108148
verify(fileDownloadService, times(1)).loadNewlyDownloadedModelFile(any());
109149
}
110150

111151
@Test
112152
public void customModel_getFile_noLocalDownloadComplete() throws Exception {
113-
when(fileDownloadService.loadNewlyDownloadedModelFile(any())).thenReturn(TEST_MODEL_FILE);
114-
assertEquals(CUSTOM_MODEL.getFile(fileDownloadService), TEST_MODEL_FILE);
153+
when(fileDownloadService.loadNewlyDownloadedModelFile(any())).thenReturn(testModelFile);
154+
assertEquals(CUSTOM_MODEL.getFile(fileDownloadService), testModelFile);
115155
verify(fileDownloadService, times(1)).loadNewlyDownloadedModelFile(any());
116156
}
117157

firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/FirebaseModelDownloaderTest.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import com.google.firebase.FirebaseOptions.Builder;
3333
import com.google.firebase.ml.modeldownloader.internal.CustomModelDownloadService;
3434
import com.google.firebase.ml.modeldownloader.internal.ModelFileDownloadService;
35+
import com.google.firebase.ml.modeldownloader.internal.ModelFileManager;
3536
import com.google.firebase.ml.modeldownloader.internal.SharedPreferencesUtil;
3637
import java.util.Collections;
3738
import java.util.Set;
@@ -67,6 +68,7 @@ public class FirebaseModelDownloaderTest {
6768
FirebaseModelDownloader firebaseModelDownloader;
6869
@Mock SharedPreferencesUtil mockPrefs;
6970
@Mock ModelFileDownloadService mockFileDownloadService;
71+
@Mock ModelFileManager mockFileManager;
7072
@Mock CustomModelDownloadService mockModelDownloadService;
7173
ExecutorService executor;
7274

@@ -84,6 +86,7 @@ public void setUp() {
8486
mockPrefs,
8587
mockFileDownloadService,
8688
mockModelDownloadService,
89+
mockFileManager,
8790
executor);
8891
}
8992

@@ -184,9 +187,17 @@ public void listDownloadedModels_returnsModelList() throws Exception {
184187
}
185188

186189
@Test
187-
public void deleteDownloadedModel_unimplemented() {
188-
assertThrows(
189-
UnsupportedOperationException.class,
190-
() -> FirebaseModelDownloader.getInstance().deleteDownloadedModel(MODEL_NAME));
190+
public void deleteDownloadedModel() throws Exception {
191+
doNothing().when(mockPrefs).clearModelDetails(eq(MODEL_NAME));
192+
doNothing().when(mockFileManager).deleteAllModels(eq(MODEL_NAME));
193+
194+
TestOnCompleteListener<Void> onCompleteListener = new TestOnCompleteListener<>();
195+
Task<Void> task = firebaseModelDownloader.deleteDownloadedModel(MODEL_NAME);
196+
task.addOnCompleteListener(executor, onCompleteListener);
197+
onCompleteListener.await();
198+
199+
assertThat(task.isComplete()).isTrue();
200+
verify(mockPrefs, times(1)).clearModelDetails(eq(MODEL_NAME));
201+
verify(mockFileManager, times(1)).deleteAllModels(eq(MODEL_NAME));
191202
}
192203
}

0 commit comments

Comments
 (0)