PackageInstallerServicepublic class PackageInstallerService extends IPackageInstaller.Stub
Fields Summary |
---|
private static final String | TAG | private static final boolean | LOGD | private static final String | TAG_SESSIONSXML constants used in {@link #mSessionsFile} | private static final String | TAG_SESSION | private static final String | ATTR_SESSION_ID | private static final String | ATTR_USER_ID | private static final String | ATTR_INSTALLER_PACKAGE_NAME | private static final String | ATTR_INSTALLER_UID | private static final String | ATTR_CREATED_MILLIS | private static final String | ATTR_SESSION_STAGE_DIR | private static final String | ATTR_SESSION_STAGE_CID | private static final String | ATTR_PREPARED | private static final String | ATTR_SEALED | private static final String | ATTR_MODE | private static final String | ATTR_INSTALL_FLAGS | private static final String | ATTR_INSTALL_LOCATION | private static final String | ATTR_SIZE_BYTES | private static final String | ATTR_APP_PACKAGE_NAME | private static final String | ATTR_APP_ICON | private static final String | ATTR_APP_LABEL | private static final String | ATTR_ORIGINATING_URI | private static final String | ATTR_REFERRER_URI | private static final String | ATTR_ABI_OVERRIDE | private static final long | MAX_AGE_MILLISAutomatically destroy sessions older than this | private static final long | MAX_ACTIVE_SESSIONSUpper bound on number of active sessions for a UID | private static final long | MAX_HISTORICAL_SESSIONSUpper bound on number of historical sessions for a UID | private final android.content.Context | mContext | private final PackageManagerService | mPm | private final android.app.AppOpsManager | mAppOps | private final File | mStagingDir | private final android.os.HandlerThread | mInstallThread | private final android.os.Handler | mInstallHandler | private final Callbacks | mCallbacks | private final android.util.AtomicFile | mSessionsFileFile storing persisted {@link #mSessions} metadata. | private final File | mSessionsDirDirectory storing persisted {@link #mSessions} metadata which is too
heavy to store directly in {@link #mSessionsFile}. | private final InternalCallback | mInternalCallback | private final Random | mRandomUsed for generating session IDs. Since this is created at boot time,
normal random might be predictable. | private final android.util.SparseArray | mSessions | private final android.util.SparseArray | mHistoricalSessionsHistorical sessions kept around for debugging purposes | private final android.util.SparseBooleanArray | mLegacySessionsSessions allocated to legacy users | private static final FilenameFilter | sStageFilter |
Constructors Summary |
---|
public PackageInstallerService(android.content.Context context, PackageManagerService pm, File stagingDir)
mContext = context;
mPm = pm;
mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
mStagingDir = stagingDir;
mInstallThread = new HandlerThread(TAG);
mInstallThread.start();
mInstallHandler = new Handler(mInstallThread.getLooper());
mCallbacks = new Callbacks(mInstallThread.getLooper());
mSessionsFile = new AtomicFile(
new File(Environment.getSystemSecureDirectory(), "install_sessions.xml"));
mSessionsDir = new File(Environment.getSystemSecureDirectory(), "install_sessions");
mSessionsDir.mkdirs();
synchronized (mSessions) {
readSessionsLocked();
final ArraySet<File> unclaimedStages = Sets.newArraySet(
mStagingDir.listFiles(sStageFilter));
final ArraySet<File> unclaimedIcons = Sets.newArraySet(
mSessionsDir.listFiles());
// Ignore stages and icons claimed by active sessions
for (int i = 0; i < mSessions.size(); i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
unclaimedStages.remove(session.stageDir);
unclaimedIcons.remove(buildAppIconFile(session.sessionId));
}
// Clean up orphaned staging directories
for (File stage : unclaimedStages) {
Slog.w(TAG, "Deleting orphan stage " + stage);
if (stage.isDirectory()) {
FileUtils.deleteContents(stage);
}
stage.delete();
}
// Clean up orphaned icons
for (File icon : unclaimedIcons) {
Slog.w(TAG, "Deleting orphan icon " + icon);
icon.delete();
}
}
|
Methods Summary |
---|
public void | abandonSession(int sessionId)
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
if (session == null || !isCallingUidOwner(session)) {
throw new SecurityException("Caller has no access to session " + sessionId);
}
session.abandon();
}
| public java.lang.String | allocateExternalStageCidLegacy()
synchronized (mSessions) {
final int sessionId = allocateSessionIdLocked();
mLegacySessions.put(sessionId, true);
return "smdl" + sessionId + ".tmp";
}
| public java.io.File | allocateInternalStageDirLegacy()
synchronized (mSessions) {
try {
final int sessionId = allocateSessionIdLocked();
mLegacySessions.put(sessionId, true);
final File stageDir = buildInternalStageDir(sessionId);
prepareInternalStageDir(stageDir);
return stageDir;
} catch (IllegalStateException e) {
throw new IOException(e);
}
}
| private int | allocateSessionIdLocked()
int n = 0;
int sessionId;
do {
sessionId = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1;
if (mSessions.get(sessionId) == null && mHistoricalSessions.get(sessionId) == null
&& !mLegacySessions.get(sessionId, false)) {
return sessionId;
}
} while (n++ < 32);
throw new IllegalStateException("Failed to allocate session ID");
| private java.io.File | buildAppIconFile(int sessionId)
return new File(mSessionsDir, "app_icon." + sessionId + ".png");
| private java.lang.String | buildExternalStageCid(int sessionId)
return "smdl" + sessionId + ".tmp";
| private java.io.File | buildInternalStageDir(int sessionId)
return new File(mStagingDir, "vmdl" + sessionId + ".tmp");
| public int | createSession(android.content.pm.PackageInstaller.SessionParams params, java.lang.String installerPackageName, int userId)
try {
return createSessionInternal(params, installerPackageName, userId);
} catch (IOException e) {
throw ExceptionUtils.wrap(e);
}
| private int | createSessionInternal(android.content.pm.PackageInstaller.SessionParams params, java.lang.String installerPackageName, int userId)
final int callingUid = Binder.getCallingUid();
mPm.enforceCrossUserPermission(callingUid, userId, true, true, "createSession");
if (mPm.isUserRestricted(userId, UserManager.DISALLOW_INSTALL_APPS)) {
throw new SecurityException("User restriction prevents installing");
}
if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) {
params.installFlags |= PackageManager.INSTALL_FROM_ADB;
} else {
mAppOps.checkPackage(callingUid, installerPackageName);
params.installFlags &= ~PackageManager.INSTALL_FROM_ADB;
params.installFlags &= ~PackageManager.INSTALL_ALL_USERS;
params.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
}
// Defensively resize giant app icons
if (params.appIcon != null) {
final ActivityManager am = (ActivityManager) mContext.getSystemService(
Context.ACTIVITY_SERVICE);
final int iconSize = am.getLauncherLargeIconSize();
if ((params.appIcon.getWidth() > iconSize * 2)
|| (params.appIcon.getHeight() > iconSize * 2)) {
params.appIcon = Bitmap.createScaledBitmap(params.appIcon, iconSize, iconSize,
true);
}
}
if (params.mode == SessionParams.MODE_FULL_INSTALL
|| params.mode == SessionParams.MODE_INHERIT_EXISTING) {
// Resolve best location for install, based on combination of
// requested install flags, delta size, and manifest settings.
final long ident = Binder.clearCallingIdentity();
try {
final int resolved = PackageHelper.resolveInstallLocation(mContext,
params.appPackageName, params.installLocation, params.sizeBytes,
params.installFlags);
if (resolved == PackageHelper.RECOMMEND_INSTALL_INTERNAL) {
params.setInstallFlagsInternal();
} else if (resolved == PackageHelper.RECOMMEND_INSTALL_EXTERNAL) {
params.setInstallFlagsExternal();
} else {
throw new IOException("No storage with enough free space; res=" + resolved);
}
} finally {
Binder.restoreCallingIdentity(ident);
}
} else {
throw new IllegalArgumentException("Invalid install mode: " + params.mode);
}
final int sessionId;
final PackageInstallerSession session;
synchronized (mSessions) {
// Sanity check that installer isn't going crazy
final int activeCount = getSessionCount(mSessions, callingUid);
if (activeCount >= MAX_ACTIVE_SESSIONS) {
throw new IllegalStateException(
"Too many active sessions for UID " + callingUid);
}
final int historicalCount = getSessionCount(mHistoricalSessions, callingUid);
if (historicalCount >= MAX_HISTORICAL_SESSIONS) {
throw new IllegalStateException(
"Too many historical sessions for UID " + callingUid);
}
final long createdMillis = System.currentTimeMillis();
sessionId = allocateSessionIdLocked();
// We're staging to exactly one location
File stageDir = null;
String stageCid = null;
if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) {
stageDir = buildInternalStageDir(sessionId);
} else {
stageCid = buildExternalStageCid(sessionId);
}
session = new PackageInstallerSession(mInternalCallback, mContext, mPm,
mInstallThread.getLooper(), sessionId, userId, installerPackageName, callingUid,
params, createdMillis, stageDir, stageCid, false, false);
mSessions.put(sessionId, session);
}
mCallbacks.notifySessionCreated(session.sessionId, session.userId);
writeSessionsAsync();
return sessionId;
| void | dump(com.android.internal.util.IndentingPrintWriter pw)
synchronized (mSessions) {
pw.println("Active install sessions:");
pw.increaseIndent();
int N = mSessions.size();
for (int i = 0; i < N; i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
session.dump(pw);
pw.println();
}
pw.println();
pw.decreaseIndent();
pw.println("Historical install sessions:");
pw.increaseIndent();
N = mHistoricalSessions.size();
for (int i = 0; i < N; i++) {
final PackageInstallerSession session = mHistoricalSessions.valueAt(i);
session.dump(pw);
pw.println();
}
pw.println();
pw.decreaseIndent();
pw.println("Legacy install sessions:");
pw.increaseIndent();
pw.println(mLegacySessions.toString());
pw.decreaseIndent();
}
| public android.content.pm.ParceledListSlice | getAllSessions(int userId)
mPm.enforceCrossUserPermission(Binder.getCallingUid(), userId, true, false, "getAllSessions");
final List<SessionInfo> result = new ArrayList<>();
synchronized (mSessions) {
for (int i = 0; i < mSessions.size(); i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
if (session.userId == userId) {
result.add(session.generateInfo());
}
}
}
return new ParceledListSlice<>(result);
| public android.content.pm.ParceledListSlice | getMySessions(java.lang.String installerPackageName, int userId)
mPm.enforceCrossUserPermission(Binder.getCallingUid(), userId, true, false, "getMySessions");
mAppOps.checkPackage(Binder.getCallingUid(), installerPackageName);
final List<SessionInfo> result = new ArrayList<>();
synchronized (mSessions) {
for (int i = 0; i < mSessions.size(); i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
if (Objects.equals(session.installerPackageName, installerPackageName)
&& session.userId == userId) {
result.add(session.generateInfo());
}
}
}
return new ParceledListSlice<>(result);
| private static int | getSessionCount(android.util.SparseArray sessions, int installerUid)
int count = 0;
final int size = sessions.size();
for (int i = 0; i < size; i++) {
final PackageInstallerSession session = sessions.valueAt(i);
if (session.installerUid == installerUid) {
count++;
}
}
return count;
| public android.content.pm.PackageInstaller.SessionInfo | getSessionInfo(int sessionId)
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
return session != null ? session.generateInfo() : null;
}
| private boolean | isCallingUidOwner(PackageInstallerSession session)
final int callingUid = Binder.getCallingUid();
if (callingUid == Process.ROOT_UID) {
return true;
} else {
return (session != null) && (callingUid == session.installerUid);
}
| public static boolean | isStageName(java.lang.String name)
final boolean isFile = name.startsWith("vmdl") && name.endsWith(".tmp");
final boolean isContainer = name.startsWith("smdl") && name.endsWith(".tmp");
final boolean isLegacyContainer = name.startsWith("smdl2tmp");
return isFile || isContainer || isLegacyContainer;
| public void | onSecureContainersAvailable()
synchronized (mSessions) {
final ArraySet<String> unclaimed = new ArraySet<>();
for (String cid : PackageHelper.getSecureContainerList()) {
if (isStageName(cid)) {
unclaimed.add(cid);
}
}
// Ignore stages claimed by active sessions
for (int i = 0; i < mSessions.size(); i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
final String cid = session.stageCid;
if (unclaimed.remove(cid)) {
// Claimed by active session, mount it
PackageHelper.mountSdDir(cid, PackageManagerService.getEncryptKey(),
Process.SYSTEM_UID);
}
}
// Clean up orphaned staging containers
for (String cid : unclaimed) {
Slog.w(TAG, "Deleting orphan container " + cid);
PackageHelper.destroySdDir(cid);
}
}
| public android.content.pm.IPackageInstallerSession | openSession(int sessionId)
try {
return openSessionInternal(sessionId);
} catch (IOException e) {
throw ExceptionUtils.wrap(e);
}
| private android.content.pm.IPackageInstallerSession | openSessionInternal(int sessionId)
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
if (session == null || !isCallingUidOwner(session)) {
throw new SecurityException("Caller has no access to session " + sessionId);
}
session.open();
return session;
}
| static void | prepareExternalStageCid(java.lang.String stageCid, long sizeBytes)
if (PackageHelper.createSdDir(sizeBytes, stageCid, PackageManagerService.getEncryptKey(),
Process.SYSTEM_UID, true) == null) {
throw new IOException("Failed to create session cid: " + stageCid);
}
| static void | prepareInternalStageDir(java.io.File stageDir)
if (stageDir.exists()) {
throw new IOException("Session dir already exists: " + stageDir);
}
try {
Os.mkdir(stageDir.getAbsolutePath(), 0755);
Os.chmod(stageDir.getAbsolutePath(), 0755);
} catch (ErrnoException e) {
// This purposefully throws if directory already exists
throw new IOException("Failed to prepare session dir: " + stageDir, e);
}
if (!SELinux.restorecon(stageDir)) {
throw new IOException("Failed to restorecon session dir: " + stageDir);
}
| private PackageInstallerSession | readSessionLocked(org.xmlpull.v1.XmlPullParser in)
final int sessionId = readIntAttribute(in, ATTR_SESSION_ID);
final int userId = readIntAttribute(in, ATTR_USER_ID);
final String installerPackageName = readStringAttribute(in, ATTR_INSTALLER_PACKAGE_NAME);
final int installerUid = readIntAttribute(in, ATTR_INSTALLER_UID,
mPm.getPackageUid(installerPackageName, userId));
final long createdMillis = readLongAttribute(in, ATTR_CREATED_MILLIS);
final String stageDirRaw = readStringAttribute(in, ATTR_SESSION_STAGE_DIR);
final File stageDir = (stageDirRaw != null) ? new File(stageDirRaw) : null;
final String stageCid = readStringAttribute(in, ATTR_SESSION_STAGE_CID);
final boolean prepared = readBooleanAttribute(in, ATTR_PREPARED, true);
final boolean sealed = readBooleanAttribute(in, ATTR_SEALED);
final SessionParams params = new SessionParams(
SessionParams.MODE_INVALID);
params.mode = readIntAttribute(in, ATTR_MODE);
params.installFlags = readIntAttribute(in, ATTR_INSTALL_FLAGS);
params.installLocation = readIntAttribute(in, ATTR_INSTALL_LOCATION);
params.sizeBytes = readLongAttribute(in, ATTR_SIZE_BYTES);
params.appPackageName = readStringAttribute(in, ATTR_APP_PACKAGE_NAME);
params.appIcon = readBitmapAttribute(in, ATTR_APP_ICON);
params.appLabel = readStringAttribute(in, ATTR_APP_LABEL);
params.originatingUri = readUriAttribute(in, ATTR_ORIGINATING_URI);
params.referrerUri = readUriAttribute(in, ATTR_REFERRER_URI);
params.abiOverride = readStringAttribute(in, ATTR_ABI_OVERRIDE);
final File appIconFile = buildAppIconFile(sessionId);
if (appIconFile.exists()) {
params.appIcon = BitmapFactory.decodeFile(appIconFile.getAbsolutePath());
params.appIconLastModified = appIconFile.lastModified();
}
return new PackageInstallerSession(mInternalCallback, mContext, mPm,
mInstallThread.getLooper(), sessionId, userId, installerPackageName, installerUid,
params, createdMillis, stageDir, stageCid, prepared, sealed);
| private void | readSessionsLocked()
if (LOGD) Slog.v(TAG, "readSessionsLocked()");
mSessions.clear();
FileInputStream fis = null;
try {
fis = mSessionsFile.openRead();
final XmlPullParser in = Xml.newPullParser();
in.setInput(fis, null);
int type;
while ((type = in.next()) != END_DOCUMENT) {
if (type == START_TAG) {
final String tag = in.getName();
if (TAG_SESSION.equals(tag)) {
final PackageInstallerSession session = readSessionLocked(in);
final long age = System.currentTimeMillis() - session.createdMillis;
final boolean valid;
if (age >= MAX_AGE_MILLIS) {
Slog.w(TAG, "Abandoning old session first created at "
+ session.createdMillis);
valid = false;
} else if (session.stageDir != null
&& !session.stageDir.exists()) {
Slog.w(TAG, "Abandoning internal session with missing stage "
+ session.stageDir);
valid = false;
} else {
valid = true;
}
if (valid) {
mSessions.put(session.sessionId, session);
} else {
// Since this is early during boot we don't send
// any observer events about the session, but we
// keep details around for dumpsys.
mHistoricalSessions.put(session.sessionId, session);
}
}
}
}
} catch (FileNotFoundException e) {
// Missing sessions are okay, probably first boot
} catch (IOException e) {
Slog.wtf(TAG, "Failed reading install sessions", e);
} catch (XmlPullParserException e) {
Slog.wtf(TAG, "Failed reading install sessions", e);
} finally {
IoUtils.closeQuietly(fis);
}
| public void | registerCallback(android.content.pm.IPackageInstallerCallback callback, int userId)
mPm.enforceCrossUserPermission(Binder.getCallingUid(), userId, true, false, "registerCallback");
mCallbacks.register(callback, userId);
| public void | setPermissionsResult(int sessionId, boolean accepted)
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INSTALL_PACKAGES, TAG);
synchronized (mSessions) {
mSessions.get(sessionId).setPermissionsResult(accepted);
}
| public void | uninstall(java.lang.String packageName, int flags, android.content.IntentSender statusReceiver, int userId)
mPm.enforceCrossUserPermission(Binder.getCallingUid(), userId, true, true, "uninstall");
final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext,
statusReceiver, packageName);
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DELETE_PACKAGES)
== PackageManager.PERMISSION_GRANTED) {
// Sweet, call straight through!
mPm.deletePackage(packageName, adapter.getBinder(), userId, flags);
} else {
// Take a short detour to confirm with user
final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE);
intent.setData(Uri.fromParts("package", packageName, null));
intent.putExtra(PackageInstaller.EXTRA_CALLBACK, adapter.getBinder().asBinder());
adapter.onUserActionRequired(intent);
}
| public void | unregisterCallback(android.content.pm.IPackageInstallerCallback callback)
mCallbacks.unregister(callback);
| public void | updateSessionAppIcon(int sessionId, android.graphics.Bitmap appIcon)
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
if (session == null || !isCallingUidOwner(session)) {
throw new SecurityException("Caller has no access to session " + sessionId);
}
// Defensively resize giant app icons
if (appIcon != null) {
final ActivityManager am = (ActivityManager) mContext.getSystemService(
Context.ACTIVITY_SERVICE);
final int iconSize = am.getLauncherLargeIconSize();
if ((appIcon.getWidth() > iconSize * 2)
|| (appIcon.getHeight() > iconSize * 2)) {
appIcon = Bitmap.createScaledBitmap(appIcon, iconSize, iconSize, true);
}
}
session.params.appIcon = appIcon;
session.params.appIconLastModified = -1;
mInternalCallback.onSessionBadgingChanged(session);
}
| public void | updateSessionAppLabel(int sessionId, java.lang.String appLabel)
synchronized (mSessions) {
final PackageInstallerSession session = mSessions.get(sessionId);
if (session == null || !isCallingUidOwner(session)) {
throw new SecurityException("Caller has no access to session " + sessionId);
}
session.params.appLabel = appLabel;
mInternalCallback.onSessionBadgingChanged(session);
}
| private void | writeSessionLocked(org.xmlpull.v1.XmlSerializer out, PackageInstallerSession session)
final SessionParams params = session.params;
out.startTag(null, TAG_SESSION);
writeIntAttribute(out, ATTR_SESSION_ID, session.sessionId);
writeIntAttribute(out, ATTR_USER_ID, session.userId);
writeStringAttribute(out, ATTR_INSTALLER_PACKAGE_NAME,
session.installerPackageName);
writeIntAttribute(out, ATTR_INSTALLER_UID, session.installerUid);
writeLongAttribute(out, ATTR_CREATED_MILLIS, session.createdMillis);
if (session.stageDir != null) {
writeStringAttribute(out, ATTR_SESSION_STAGE_DIR,
session.stageDir.getAbsolutePath());
}
if (session.stageCid != null) {
writeStringAttribute(out, ATTR_SESSION_STAGE_CID, session.stageCid);
}
writeBooleanAttribute(out, ATTR_PREPARED, session.isPrepared());
writeBooleanAttribute(out, ATTR_SEALED, session.isSealed());
writeIntAttribute(out, ATTR_MODE, params.mode);
writeIntAttribute(out, ATTR_INSTALL_FLAGS, params.installFlags);
writeIntAttribute(out, ATTR_INSTALL_LOCATION, params.installLocation);
writeLongAttribute(out, ATTR_SIZE_BYTES, params.sizeBytes);
writeStringAttribute(out, ATTR_APP_PACKAGE_NAME, params.appPackageName);
writeStringAttribute(out, ATTR_APP_LABEL, params.appLabel);
writeUriAttribute(out, ATTR_ORIGINATING_URI, params.originatingUri);
writeUriAttribute(out, ATTR_REFERRER_URI, params.referrerUri);
writeStringAttribute(out, ATTR_ABI_OVERRIDE, params.abiOverride);
// Persist app icon if changed since last written
final File appIconFile = buildAppIconFile(session.sessionId);
if (params.appIcon == null && appIconFile.exists()) {
appIconFile.delete();
} else if (params.appIcon != null
&& appIconFile.lastModified() != params.appIconLastModified) {
if (LOGD) Slog.w(TAG, "Writing changed icon " + appIconFile);
FileOutputStream os = null;
try {
os = new FileOutputStream(appIconFile);
params.appIcon.compress(CompressFormat.PNG, 90, os);
} catch (IOException e) {
Slog.w(TAG, "Failed to write icon " + appIconFile + ": " + e.getMessage());
} finally {
IoUtils.closeQuietly(os);
}
params.appIconLastModified = appIconFile.lastModified();
}
out.endTag(null, TAG_SESSION);
| private void | writeSessionsAsync()
IoThread.getHandler().post(new Runnable() {
@Override
public void run() {
synchronized (mSessions) {
writeSessionsLocked();
}
}
});
| private void | writeSessionsLocked()
if (LOGD) Slog.v(TAG, "writeSessionsLocked()");
FileOutputStream fos = null;
try {
fos = mSessionsFile.startWrite();
XmlSerializer out = new FastXmlSerializer();
out.setOutput(fos, "utf-8");
out.startDocument(null, true);
out.startTag(null, TAG_SESSIONS);
final int size = mSessions.size();
for (int i = 0; i < size; i++) {
final PackageInstallerSession session = mSessions.valueAt(i);
writeSessionLocked(out, session);
}
out.endTag(null, TAG_SESSIONS);
out.endDocument();
mSessionsFile.finishWrite(fos);
} catch (IOException e) {
if (fos != null) {
mSessionsFile.failWrite(fos);
}
}
|
|