Skip to content

Commit 11b9b63

Browse files
authored
Merge branch 'master' into backgroundUpdate
2 parents 7e5e5c2 + c75caee commit 11b9b63

File tree

14 files changed

+240
-114
lines changed

14 files changed

+240
-114
lines changed

firebase-database/src/main/java/com/google/firebase/database/Query.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ public ChildEventListener addChildEventListener(@NonNull ChildEventListener list
163163
}
164164

165165
/**
166-
* Get the server value for this query, updating cache and raising events if successful. If not
167-
* connected, fall back to a locally-cached value.
166+
* Gets the server values for this query. Updates the cache and raises events if successful. If not
167+
* connected, falls back to a locally-cached value.
168168
*/
169169
@NonNull
170170
public Task<DataSnapshot> get() {

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

@@ -227,7 +232,16 @@ public Task<Set<CustomModel>> listDownloadedModels() {
227232
*/
228233
@NonNull
229234
public Task<Void> deleteDownloadedModel(@NonNull String modelName) {
230-
throw new UnsupportedOperationException("Not yet implemented.");
235+
236+
TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();
237+
executor.execute(
238+
() -> {
239+
// remove all files associated with this model and then clean up model references.
240+
fileManager.deleteAllModels(modelName);
241+
sharedPreferencesUtil.clearModelDetails(modelName);
242+
taskCompletionSource.setResult(null);
243+
});
244+
return taskCompletionSource.getTask();
231245
}
232246

233247
/** 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: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
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;
@@ -37,7 +39,7 @@
3739
public class ModelFileManager {
3840

3941
public static final String CUSTOM_MODEL_ROOT_PATH = "com.google.firebase.ml.custom.models";
40-
42+
private static final String TAG = "FirebaseModelFileManage";
4143
private static final int INVALID_INDEX = -1;
4244
private final Context context;
4345
private final FirebaseApp firebaseApp;
@@ -111,7 +113,7 @@ private int getLatestCachedModelVersion(@NonNull File modelDir) {
111113
try {
112114
index = Math.max(index, Integer.parseInt(modelFile.getName()));
113115
} catch (NumberFormatException e) {
114-
System.out.println("Contains non-integer file name " + modelFile.getName());
116+
Log.d(TAG, String.format("Contains non-integer file name %s", modelFile.getName()));
115117
}
116118
}
117119
return index;
@@ -121,6 +123,19 @@ private int getLatestCachedModelVersion(@NonNull File modelDir) {
121123
@Nullable
122124
File getModelFileDestination(@NonNull CustomModel model) throws Exception {
123125
File destFolder = getDirImpl(model.getName());
126+
if (!destFolder.exists()) {
127+
Log.d(TAG, "model folder does not exist, creating one: " + destFolder.getAbsolutePath());
128+
if (!destFolder.mkdirs()) {
129+
// TODO(annzimmer) change to FirebaseMLException when logging complete. Add test at same
130+
// time.
131+
throw new Exception("Failed to create model folder: " + destFolder);
132+
}
133+
} else if (!destFolder.isDirectory()) {
134+
// TODO(annzimmer) change to FirebaseMLException when logging complete. Add test at same time.
135+
throw new Exception(
136+
"Can not create model folder, since an existing file has the same name: " + destFolder);
137+
}
138+
124139
int index = getLatestCachedModelVersion(destFolder);
125140
return new File(destFolder, String.valueOf(index + 1));
126141
}
@@ -144,6 +159,11 @@ public synchronized File moveModelToDestinationFolder(
144159
@NonNull CustomModel customModel, @NonNull ParcelFileDescriptor modelFileDescriptor)
145160
throws Exception {
146161
File modelFileDestination = getModelFileDestination(customModel);
162+
// why would this ever be true?
163+
File modelFolder = modelFileDestination.getParentFile();
164+
if (!modelFolder.exists()) {
165+
modelFolder.mkdirs();
166+
}
147167

148168
// Moves to the final destination file in app private folder to avoid the downloaded file from
149169
// being changed by
@@ -159,11 +179,44 @@ public synchronized File moveModelToDestinationFolder(
159179
fos.getFD().sync();
160180
} catch (IOException e) {
161181
// Failed to copy to destination - clean up.
162-
System.out.println("Failed to copy downloaded model file to destination folder: " + e);
182+
Log.d(TAG, "Failed to copy downloaded model file to destination folder: " + e.toString());
163183
modelFileDestination.delete();
164184
return null;
165185
}
166186

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

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: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public class FirebaseModelDownloaderTest {
8080
private @Mock SharedPreferencesUtil mockPrefs;
8181
private @Mock ModelFileDownloadService mockFileDownloadService;
8282
private @Mock CustomModelDownloadService mockModelDownloadService;
83+
private @Mock ModelFileManager mockFileManager;
8384

8485
private FirebaseModelDownloader firebaseModelDownloader;
8586
private ExecutorService executor;
@@ -105,6 +106,7 @@ public void setUp() throws Exception {
105106
mockPrefs,
106107
mockFileDownloadService,
107108
mockModelDownloadService,
109+
mockFileManager,
108110
executor);
109111
setUpTestingFiles(app);
110112
}
@@ -471,9 +473,17 @@ public void listDownloadedModels_returnsModelList() throws Exception {
471473
}
472474

473475
@Test
474-
public void deleteDownloadedModel_unimplemented() {
475-
assertThrows(
476-
UnsupportedOperationException.class,
477-
() -> FirebaseModelDownloader.getInstance().deleteDownloadedModel(MODEL_NAME));
476+
public void deleteDownloadedModel() throws Exception {
477+
doNothing().when(mockPrefs).clearModelDetails(eq(MODEL_NAME));
478+
doNothing().when(mockFileManager).deleteAllModels(eq(MODEL_NAME));
479+
480+
TestOnCompleteListener<Void> onCompleteListener = new TestOnCompleteListener<>();
481+
Task<Void> task = firebaseModelDownloader.deleteDownloadedModel(MODEL_NAME);
482+
task.addOnCompleteListener(executor, onCompleteListener);
483+
onCompleteListener.await();
484+
485+
assertThat(task.isComplete()).isTrue();
486+
verify(mockPrefs, times(1)).clearModelDetails(eq(MODEL_NAME));
487+
verify(mockFileManager, times(1)).deleteAllModels(eq(MODEL_NAME));
478488
}
479489
}

firebase-ml-modeldownloader/src/test/java/com/google/firebase/ml/modeldownloader/internal/ModelFileDownloadServiceTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public void setUp() {
103103

104104
executor = Executors.newSingleThreadExecutor();
105105
sharedPreferencesUtil = new SharedPreferencesUtil(app);
106-
sharedPreferencesUtil.clearModelDetails(MODEL_NAME, false);
106+
sharedPreferencesUtil.clearModelDetails(MODEL_NAME);
107107

108108
modelFileDownloadService =
109109
new ModelFileDownloadService(

0 commit comments

Comments
 (0)