1 /*
2  * Copyright 2020 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 package com.android.server.blob;
17 
18 import static android.app.blob.BlobStoreManager.COMMIT_RESULT_ERROR;
19 import static android.app.blob.XmlTags.ATTR_CREATION_TIME_MS;
20 import static android.app.blob.XmlTags.ATTR_ID;
21 import static android.app.blob.XmlTags.ATTR_PACKAGE;
22 import static android.app.blob.XmlTags.ATTR_UID;
23 import static android.app.blob.XmlTags.TAG_ACCESS_MODE;
24 import static android.app.blob.XmlTags.TAG_BLOB_HANDLE;
25 import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER;
26 import static android.system.OsConstants.O_CREAT;
27 import static android.system.OsConstants.O_RDONLY;
28 import static android.system.OsConstants.O_RDWR;
29 import static android.system.OsConstants.SEEK_SET;
30 import static android.text.format.Formatter.FLAG_IEC_UNITS;
31 import static android.text.format.Formatter.formatFileSize;
32 
33 import static com.android.server.blob.BlobStoreConfig.TAG;
34 import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_SESSION_CREATION_TIME;
35 import static com.android.server.blob.BlobStoreConfig.getMaxPermittedPackages;
36 import static com.android.server.blob.BlobStoreConfig.hasSessionExpired;
37 
38 import android.annotation.BytesLong;
39 import android.annotation.NonNull;
40 import android.annotation.Nullable;
41 import android.app.blob.BlobHandle;
42 import android.app.blob.IBlobCommitCallback;
43 import android.app.blob.IBlobStoreSession;
44 import android.content.Context;
45 import android.os.Binder;
46 import android.os.FileUtils;
47 import android.os.LimitExceededException;
48 import android.os.ParcelFileDescriptor;
49 import android.os.ParcelableException;
50 import android.os.RemoteException;
51 import android.os.RevocableFileDescriptor;
52 import android.os.Trace;
53 import android.os.storage.StorageManager;
54 import android.system.ErrnoException;
55 import android.system.Os;
56 import android.util.ExceptionUtils;
57 import android.util.IndentingPrintWriter;
58 import android.util.Slog;
59 
60 import com.android.internal.annotations.GuardedBy;
61 import com.android.internal.annotations.VisibleForTesting;
62 import com.android.internal.util.FrameworkStatsLog;
63 import com.android.internal.util.Preconditions;
64 import com.android.internal.util.XmlUtils;
65 import com.android.server.blob.BlobStoreManagerService.DumpArgs;
66 import com.android.server.blob.BlobStoreManagerService.SessionStateChangeListener;
67 
68 import libcore.io.IoUtils;
69 
70 import org.xmlpull.v1.XmlPullParser;
71 import org.xmlpull.v1.XmlPullParserException;
72 import org.xmlpull.v1.XmlSerializer;
73 
74 import java.io.File;
75 import java.io.FileDescriptor;
76 import java.io.IOException;
77 import java.security.NoSuchAlgorithmException;
78 import java.util.ArrayList;
79 import java.util.Arrays;
80 import java.util.Objects;
81 
82 /**
83  * Class to represent the state corresponding to an ongoing
84  * {@link android.app.blob.BlobStoreManager.Session}
85  */
86 @VisibleForTesting
87 class BlobStoreSession extends IBlobStoreSession.Stub {
88 
89     static final int STATE_OPENED = 1;
90     static final int STATE_CLOSED = 0;
91     static final int STATE_ABANDONED = 2;
92     static final int STATE_COMMITTED = 3;
93     static final int STATE_VERIFIED_VALID = 4;
94     static final int STATE_VERIFIED_INVALID = 5;
95 
96     private final Object mSessionLock = new Object();
97 
98     private final Context mContext;
99     private final SessionStateChangeListener mListener;
100 
101     private final BlobHandle mBlobHandle;
102     private final long mSessionId;
103     private final int mOwnerUid;
104     private final String mOwnerPackageName;
105     private final long mCreationTimeMs;
106 
107     // Do not access this directly, instead use getSessionFile().
108     private File mSessionFile;
109 
110     @GuardedBy("mRevocableFds")
111     private final ArrayList<RevocableFileDescriptor> mRevocableFds = new ArrayList<>();
112 
113     // This will be accessed from only one thread at any point of time, so no need to grab
114     // a lock for this.
115     private byte[] mDataDigest;
116 
117     @GuardedBy("mSessionLock")
118     private int mState = STATE_CLOSED;
119 
120     @GuardedBy("mSessionLock")
121     private final BlobAccessMode mBlobAccessMode = new BlobAccessMode();
122 
123     @GuardedBy("mSessionLock")
124     private IBlobCommitCallback mBlobCommitCallback;
125 
BlobStoreSession(Context context, long sessionId, BlobHandle blobHandle, int ownerUid, String ownerPackageName, long creationTimeMs, SessionStateChangeListener listener)126     private BlobStoreSession(Context context, long sessionId, BlobHandle blobHandle,
127             int ownerUid, String ownerPackageName, long creationTimeMs,
128             SessionStateChangeListener listener) {
129         this.mContext = context;
130         this.mBlobHandle = blobHandle;
131         this.mSessionId = sessionId;
132         this.mOwnerUid = ownerUid;
133         this.mOwnerPackageName = ownerPackageName;
134         this.mCreationTimeMs = creationTimeMs;
135         this.mListener = listener;
136     }
137 
BlobStoreSession(Context context, long sessionId, BlobHandle blobHandle, int ownerUid, String ownerPackageName, SessionStateChangeListener listener)138     BlobStoreSession(Context context, long sessionId, BlobHandle blobHandle,
139             int ownerUid, String ownerPackageName, SessionStateChangeListener listener) {
140         this(context, sessionId, blobHandle, ownerUid, ownerPackageName,
141                 System.currentTimeMillis(), listener);
142     }
143 
getBlobHandle()144     public BlobHandle getBlobHandle() {
145         return mBlobHandle;
146     }
147 
getSessionId()148     public long getSessionId() {
149         return mSessionId;
150     }
151 
getOwnerUid()152     public int getOwnerUid() {
153         return mOwnerUid;
154     }
155 
getOwnerPackageName()156     public String getOwnerPackageName() {
157         return mOwnerPackageName;
158     }
159 
hasAccess(int callingUid, String callingPackageName)160     boolean hasAccess(int callingUid, String callingPackageName) {
161         return mOwnerUid == callingUid && mOwnerPackageName.equals(callingPackageName);
162     }
163 
open()164     void open() {
165         synchronized (mSessionLock) {
166             if (isFinalized()) {
167                 throw new IllegalStateException("Not allowed to open session with state: "
168                         + stateToString(mState));
169             }
170             mState = STATE_OPENED;
171         }
172     }
173 
getState()174     int getState() {
175         synchronized (mSessionLock) {
176             return mState;
177         }
178     }
179 
sendCommitCallbackResult(int result)180     void sendCommitCallbackResult(int result) {
181         synchronized (mSessionLock) {
182             try {
183                 mBlobCommitCallback.onResult(result);
184             } catch (RemoteException e) {
185                 Slog.d(TAG, "Error sending the callback result", e);
186             }
187             mBlobCommitCallback = null;
188         }
189     }
190 
getBlobAccessMode()191     BlobAccessMode getBlobAccessMode() {
192         synchronized (mSessionLock) {
193             return mBlobAccessMode;
194         }
195     }
196 
isFinalized()197     boolean isFinalized() {
198         synchronized (mSessionLock) {
199             return mState == STATE_COMMITTED || mState == STATE_ABANDONED;
200         }
201     }
202 
isExpired()203     boolean isExpired() {
204         final long lastModifiedTimeMs = getSessionFile().lastModified();
205         return hasSessionExpired(lastModifiedTimeMs == 0
206                 ? mCreationTimeMs : lastModifiedTimeMs);
207     }
208 
209     @Override
210     @NonNull
openWrite(@ytesLong long offsetBytes, @BytesLong long lengthBytes)211     public ParcelFileDescriptor openWrite(@BytesLong long offsetBytes,
212             @BytesLong long lengthBytes) {
213         Preconditions.checkArgumentNonnegative(offsetBytes, "offsetBytes must not be negative");
214 
215         assertCallerIsOwner();
216         synchronized (mSessionLock) {
217             if (mState != STATE_OPENED) {
218                 throw new IllegalStateException("Not allowed to write in state: "
219                         + stateToString(mState));
220             }
221         }
222 
223         FileDescriptor fd = null;
224         try {
225             fd = openWriteInternal(offsetBytes, lengthBytes);
226             final RevocableFileDescriptor revocableFd = new RevocableFileDescriptor(mContext, fd);
227             synchronized (mSessionLock) {
228                 if (mState != STATE_OPENED) {
229                     IoUtils.closeQuietly(fd);
230                     throw new IllegalStateException("Not allowed to write in state: "
231                             + stateToString(mState));
232                 }
233                 trackRevocableFdLocked(revocableFd);
234                 return revocableFd.getRevocableFileDescriptor();
235             }
236         } catch (IOException e) {
237             IoUtils.closeQuietly(fd);
238             throw ExceptionUtils.wrap(e);
239         }
240     }
241 
242     @NonNull
openWriteInternal(@ytesLong long offsetBytes, @BytesLong long lengthBytes)243     private FileDescriptor openWriteInternal(@BytesLong long offsetBytes,
244             @BytesLong long lengthBytes) throws IOException {
245         // TODO: Add limit on active open sessions/writes/reads
246         try {
247             final File sessionFile = getSessionFile();
248             if (sessionFile == null) {
249                 throw new IllegalStateException("Couldn't get the file for this session");
250             }
251             final FileDescriptor fd = Os.open(sessionFile.getPath(), O_CREAT | O_RDWR, 0600);
252             if (offsetBytes > 0) {
253                 final long curOffset = Os.lseek(fd, offsetBytes, SEEK_SET);
254                 if (curOffset != offsetBytes) {
255                     throw new IllegalStateException("Failed to seek " + offsetBytes
256                             + "; curOffset=" + offsetBytes);
257                 }
258             }
259             if (lengthBytes > 0) {
260                 mContext.getSystemService(StorageManager.class).allocateBytes(fd, lengthBytes);
261             }
262             return fd;
263         } catch (ErrnoException e) {
264             throw e.rethrowAsIOException();
265         }
266     }
267 
268     @Override
269     @NonNull
openRead()270     public ParcelFileDescriptor openRead() {
271         assertCallerIsOwner();
272         synchronized (mSessionLock) {
273             if (mState != STATE_OPENED) {
274                 throw new IllegalStateException("Not allowed to read in state: "
275                         + stateToString(mState));
276             }
277             if (!BlobStoreConfig.shouldUseRevocableFdForReads()) {
278                 try {
279                     return new ParcelFileDescriptor(openReadInternal());
280                 } catch (IOException e) {
281                     throw ExceptionUtils.wrap(e);
282                 }
283             }
284         }
285 
286         FileDescriptor fd = null;
287         try {
288             fd = openReadInternal();
289             final RevocableFileDescriptor revocableFd = new RevocableFileDescriptor(mContext, fd);
290             synchronized (mSessionLock) {
291                 if (mState != STATE_OPENED) {
292                     IoUtils.closeQuietly(fd);
293                     throw new IllegalStateException("Not allowed to read in state: "
294                             + stateToString(mState));
295                 }
296                 trackRevocableFdLocked(revocableFd);
297                 return revocableFd.getRevocableFileDescriptor();
298             }
299         } catch (IOException e) {
300             IoUtils.closeQuietly(fd);
301             throw ExceptionUtils.wrap(e);
302         }
303     }
304 
305     @NonNull
openReadInternal()306     private FileDescriptor openReadInternal() throws IOException {
307         try {
308             final File sessionFile = getSessionFile();
309             if (sessionFile == null) {
310                 throw new IllegalStateException("Couldn't get the file for this session");
311             }
312             final FileDescriptor fd = Os.open(sessionFile.getPath(), O_RDONLY, 0);
313             return fd;
314         } catch (ErrnoException e) {
315             throw e.rethrowAsIOException();
316         }
317     }
318 
319     @Override
320     @BytesLong
getSize()321     public long getSize() {
322         return getSessionFile().length();
323     }
324 
325     @Override
allowPackageAccess(@onNull String packageName, @NonNull byte[] certificate)326     public void allowPackageAccess(@NonNull String packageName,
327             @NonNull byte[] certificate) {
328         assertCallerIsOwner();
329         Objects.requireNonNull(packageName, "packageName must not be null");
330         synchronized (mSessionLock) {
331             if (mState != STATE_OPENED) {
332                 throw new IllegalStateException("Not allowed to change access type in state: "
333                         + stateToString(mState));
334             }
335             if (mBlobAccessMode.getAllowedPackagesCount() >= getMaxPermittedPackages()) {
336                 throw new ParcelableException(new LimitExceededException(
337                         "Too many packages permitted to access the blob: "
338                                 + mBlobAccessMode.getAllowedPackagesCount()));
339             }
340             mBlobAccessMode.allowPackageAccess(packageName, certificate);
341         }
342     }
343 
344     @Override
allowSameSignatureAccess()345     public void allowSameSignatureAccess() {
346         assertCallerIsOwner();
347         synchronized (mSessionLock) {
348             if (mState != STATE_OPENED) {
349                 throw new IllegalStateException("Not allowed to change access type in state: "
350                         + stateToString(mState));
351             }
352             mBlobAccessMode.allowSameSignatureAccess();
353         }
354     }
355 
356     @Override
allowPublicAccess()357     public void allowPublicAccess() {
358         assertCallerIsOwner();
359         synchronized (mSessionLock) {
360             if (mState != STATE_OPENED) {
361                 throw new IllegalStateException("Not allowed to change access type in state: "
362                         + stateToString(mState));
363             }
364             mBlobAccessMode.allowPublicAccess();
365         }
366     }
367 
368     @Override
isPackageAccessAllowed(@onNull String packageName, @NonNull byte[] certificate)369     public boolean isPackageAccessAllowed(@NonNull String packageName,
370             @NonNull byte[] certificate) {
371         assertCallerIsOwner();
372         Objects.requireNonNull(packageName, "packageName must not be null");
373         Preconditions.checkByteArrayNotEmpty(certificate, "certificate");
374 
375         synchronized (mSessionLock) {
376             if (mState != STATE_OPENED) {
377                 throw new IllegalStateException("Not allowed to get access type in state: "
378                         + stateToString(mState));
379             }
380             return mBlobAccessMode.isPackageAccessAllowed(packageName, certificate);
381         }
382     }
383 
384     @Override
isSameSignatureAccessAllowed()385     public boolean isSameSignatureAccessAllowed() {
386         assertCallerIsOwner();
387         synchronized (mSessionLock) {
388             if (mState != STATE_OPENED) {
389                 throw new IllegalStateException("Not allowed to get access type in state: "
390                         + stateToString(mState));
391             }
392             return mBlobAccessMode.isSameSignatureAccessAllowed();
393         }
394     }
395 
396     @Override
isPublicAccessAllowed()397     public boolean isPublicAccessAllowed() {
398         assertCallerIsOwner();
399         synchronized (mSessionLock) {
400             if (mState != STATE_OPENED) {
401                 throw new IllegalStateException("Not allowed to get access type in state: "
402                         + stateToString(mState));
403             }
404             return mBlobAccessMode.isPublicAccessAllowed();
405         }
406     }
407 
408     @Override
close()409     public void close() {
410         closeSession(STATE_CLOSED, false /* sendCallback */);
411     }
412 
413     @Override
abandon()414     public void abandon() {
415         closeSession(STATE_ABANDONED, true /* sendCallback */);
416     }
417 
418     @Override
commit(IBlobCommitCallback callback)419     public void commit(IBlobCommitCallback callback) {
420         synchronized (mSessionLock) {
421             mBlobCommitCallback = callback;
422 
423             closeSession(STATE_COMMITTED, true /* sendCallback */);
424         }
425     }
426 
closeSession(int state, boolean sendCallback)427     private void closeSession(int state, boolean sendCallback) {
428         assertCallerIsOwner();
429         synchronized (mSessionLock) {
430             if (mState != STATE_OPENED) {
431                 if (state == STATE_CLOSED) {
432                     // Just trying to close the session which is already deleted or abandoned,
433                     // ignore.
434                     return;
435                 } else {
436                     throw new IllegalStateException("Not allowed to delete or abandon a session"
437                             + " with state: " + stateToString(mState));
438                 }
439             }
440 
441             mState = state;
442             revokeAllFds();
443 
444             if (sendCallback) {
445                 mListener.onStateChanged(this);
446             }
447         }
448     }
449 
computeDigest()450     void computeDigest() {
451         try {
452             Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER,
453                     "computeBlobDigest-i" + mSessionId + "-l" + getSessionFile().length());
454             mDataDigest = FileUtils.digest(getSessionFile(), mBlobHandle.algorithm);
455         } catch (IOException | NoSuchAlgorithmException e) {
456             Slog.e(TAG, "Error computing the digest", e);
457         } finally {
458             Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER);
459         }
460     }
461 
verifyBlobData()462     void verifyBlobData() {
463         synchronized (mSessionLock) {
464             if (mDataDigest != null && Arrays.equals(mDataDigest, mBlobHandle.digest)) {
465                 mState = STATE_VERIFIED_VALID;
466                 // Commit callback will be sent once the data is persisted.
467             } else {
468                 Slog.d(TAG, "Digest of the data ("
469                         + (mDataDigest == null ? "null" : BlobHandle.safeDigest(mDataDigest))
470                         + ") didn't match the given BlobHandle.digest ("
471                         + BlobHandle.safeDigest(mBlobHandle.digest) + ")");
472                 mState = STATE_VERIFIED_INVALID;
473 
474                 FrameworkStatsLog.write(FrameworkStatsLog.BLOB_COMMITTED, getOwnerUid(), mSessionId,
475                         getSize(), FrameworkStatsLog.BLOB_COMMITTED__RESULT__DIGEST_MISMATCH);
476                 sendCommitCallbackResult(COMMIT_RESULT_ERROR);
477             }
478             mListener.onStateChanged(this);
479         }
480     }
481 
destroy()482     void destroy() {
483         revokeAllFds();
484         getSessionFile().delete();
485     }
486 
revokeAllFds()487     private void revokeAllFds() {
488         synchronized (mRevocableFds) {
489             for (int i = mRevocableFds.size() - 1; i >= 0; --i) {
490                 mRevocableFds.get(i).revoke();
491             }
492             mRevocableFds.clear();
493         }
494     }
495 
496     @GuardedBy("mSessionLock")
trackRevocableFdLocked(RevocableFileDescriptor revocableFd)497     private void trackRevocableFdLocked(RevocableFileDescriptor revocableFd) {
498         synchronized (mRevocableFds) {
499             mRevocableFds.add(revocableFd);
500         }
501         revocableFd.addOnCloseListener((e) -> {
502             synchronized (mRevocableFds) {
503                 mRevocableFds.remove(revocableFd);
504             }
505         });
506     }
507 
508     @Nullable
getSessionFile()509     File getSessionFile() {
510         if (mSessionFile == null) {
511             mSessionFile = BlobStoreConfig.prepareBlobFile(mSessionId);
512         }
513         return mSessionFile;
514     }
515 
516     @NonNull
stateToString(int state)517     static String stateToString(int state) {
518         switch (state) {
519             case STATE_OPENED:
520                 return "<opened>";
521             case STATE_CLOSED:
522                 return "<closed>";
523             case STATE_ABANDONED:
524                 return "<abandoned>";
525             case STATE_COMMITTED:
526                 return "<committed>";
527             case STATE_VERIFIED_VALID:
528                 return "<verified_valid>";
529             case STATE_VERIFIED_INVALID:
530                 return "<verified_invalid>";
531             default:
532                 Slog.wtf(TAG, "Unknown state: " + state);
533                 return "<unknown>";
534         }
535     }
536 
537     @Override
toString()538     public String toString() {
539         return "BlobStoreSession {"
540                 + "id:" + mSessionId
541                 + ",handle:" + mBlobHandle
542                 + ",uid:" + mOwnerUid
543                 + ",pkg:" + mOwnerPackageName
544                 + "}";
545     }
546 
assertCallerIsOwner()547     private void assertCallerIsOwner() {
548         final int callingUid = Binder.getCallingUid();
549         if (callingUid != mOwnerUid) {
550             throw new SecurityException(mOwnerUid + " is not the session owner");
551         }
552     }
553 
dump(IndentingPrintWriter fout, DumpArgs dumpArgs)554     void dump(IndentingPrintWriter fout, DumpArgs dumpArgs) {
555         synchronized (mSessionLock) {
556             fout.println("state: " + stateToString(mState));
557             fout.println("ownerUid: " + mOwnerUid);
558             fout.println("ownerPkg: " + mOwnerPackageName);
559             fout.println("creation time: " + BlobStoreUtils.formatTime(mCreationTimeMs));
560             fout.println("size: " + formatFileSize(mContext, getSize(), FLAG_IEC_UNITS));
561 
562             fout.println("blobHandle:");
563             fout.increaseIndent();
564             mBlobHandle.dump(fout, dumpArgs.shouldDumpFull());
565             fout.decreaseIndent();
566 
567             fout.println("accessMode:");
568             fout.increaseIndent();
569             mBlobAccessMode.dump(fout);
570             fout.decreaseIndent();
571 
572             fout.println("Open fds: #" + mRevocableFds.size());
573         }
574     }
575 
writeToXml(@onNull XmlSerializer out)576     void writeToXml(@NonNull XmlSerializer out) throws IOException {
577         synchronized (mSessionLock) {
578             XmlUtils.writeLongAttribute(out, ATTR_ID, mSessionId);
579             XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, mOwnerPackageName);
580             XmlUtils.writeIntAttribute(out, ATTR_UID, mOwnerUid);
581             XmlUtils.writeLongAttribute(out, ATTR_CREATION_TIME_MS, mCreationTimeMs);
582 
583             out.startTag(null, TAG_BLOB_HANDLE);
584             mBlobHandle.writeToXml(out);
585             out.endTag(null, TAG_BLOB_HANDLE);
586 
587             out.startTag(null, TAG_ACCESS_MODE);
588             mBlobAccessMode.writeToXml(out);
589             out.endTag(null, TAG_ACCESS_MODE);
590         }
591     }
592 
593     @Nullable
createFromXml(@onNull XmlPullParser in, int version, @NonNull Context context, @NonNull SessionStateChangeListener stateChangeListener)594     static BlobStoreSession createFromXml(@NonNull XmlPullParser in, int version,
595             @NonNull Context context, @NonNull SessionStateChangeListener stateChangeListener)
596             throws IOException, XmlPullParserException {
597         final long sessionId = XmlUtils.readLongAttribute(in, ATTR_ID);
598         final String ownerPackageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE);
599         final int ownerUid = XmlUtils.readIntAttribute(in, ATTR_UID);
600         final long creationTimeMs = version >= XML_VERSION_ADD_SESSION_CREATION_TIME
601                 ? XmlUtils.readLongAttribute(in, ATTR_CREATION_TIME_MS)
602                 : System.currentTimeMillis();
603 
604         final int depth = in.getDepth();
605         BlobHandle blobHandle = null;
606         BlobAccessMode blobAccessMode = null;
607         while (XmlUtils.nextElementWithin(in, depth)) {
608             if (TAG_BLOB_HANDLE.equals(in.getName())) {
609                 blobHandle = BlobHandle.createFromXml(in);
610             } else if (TAG_ACCESS_MODE.equals(in.getName())) {
611                 blobAccessMode = BlobAccessMode.createFromXml(in);
612             }
613         }
614 
615         if (blobHandle == null) {
616             Slog.wtf(TAG, "blobHandle should be available");
617             return null;
618         }
619         if (blobAccessMode == null) {
620             Slog.wtf(TAG, "blobAccessMode should be available");
621             return null;
622         }
623 
624         final BlobStoreSession blobStoreSession = new BlobStoreSession(context, sessionId,
625                 blobHandle, ownerUid, ownerPackageName, creationTimeMs, stateChangeListener);
626         blobStoreSession.mBlobAccessMode.allow(blobAccessMode);
627         return blobStoreSession;
628     }
629 }
630