Attach protobuf tombstones to ApplicationExitInfo.
Bug: http://b/159164105 Test: atest CtsAppExitTestCases Change-Id: I377247e8fc85f917676cb57eaa3128b955634fa5
This commit is contained in:
@@ -428,6 +428,13 @@ public final class ApplicationExitInfo implements Parcelable {
|
||||
*/
|
||||
private IAppTraceRetriever mAppTraceRetriever;
|
||||
|
||||
/**
|
||||
* ParcelFileDescriptor pointing to a native tombstone.
|
||||
*
|
||||
* @see #getTraceInputStream
|
||||
*/
|
||||
private IParcelFileDescriptorRetriever mNativeTombstoneRetriever;
|
||||
|
||||
/** @hide */
|
||||
@IntDef(prefix = { "REASON_" }, value = {
|
||||
REASON_UNKNOWN,
|
||||
@@ -603,22 +610,38 @@ public final class ApplicationExitInfo implements Parcelable {
|
||||
* prior to the death of the process; typically it'll be available when
|
||||
* the reason is {@link #REASON_ANR}, though if the process gets an ANR
|
||||
* but recovers, and dies for another reason later, this trace will be included
|
||||
* in the record of {@link ApplicationExitInfo} still.
|
||||
* in the record of {@link ApplicationExitInfo} still. Beginning with API 31,
|
||||
* tombstone traces will be returned for
|
||||
* {@link #REASON_CRASH_NATIVE}, with an InputStream containing a protobuf with
|
||||
* <a href="https://android.googlesource.com/platform/system/core/+/refs/heads/master/debuggerd/proto/tombstone.proto">this schema</a>.
|
||||
* Note thatbecause these traces are kept in a separate global circular buffer, crashes may be
|
||||
* overwritten by newer crashes (including from other applications), so this may still return
|
||||
* null.
|
||||
*
|
||||
* @return The input stream to the traces that was taken by the system
|
||||
* prior to the death of the process.
|
||||
*/
|
||||
public @Nullable InputStream getTraceInputStream() throws IOException {
|
||||
if (mAppTraceRetriever == null) {
|
||||
if (mAppTraceRetriever == null && mNativeTombstoneRetriever == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final ParcelFileDescriptor fd = mAppTraceRetriever.getTraceFileDescriptor(
|
||||
mPackageName, mPackageUid, mPid);
|
||||
if (fd == null) {
|
||||
return null;
|
||||
if (mNativeTombstoneRetriever != null) {
|
||||
final ParcelFileDescriptor pfd = mNativeTombstoneRetriever.getPfd();
|
||||
if (pfd == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ParcelFileDescriptor.AutoCloseInputStream(pfd);
|
||||
} else {
|
||||
final ParcelFileDescriptor fd = mAppTraceRetriever.getTraceFileDescriptor(
|
||||
mPackageName, mPackageUid, mPid);
|
||||
if (fd == null) {
|
||||
return null;
|
||||
}
|
||||
return new GZIPInputStream(new ParcelFileDescriptor.AutoCloseInputStream(fd));
|
||||
}
|
||||
return new GZIPInputStream(new ParcelFileDescriptor.AutoCloseInputStream(fd));
|
||||
} catch (RemoteException e) {
|
||||
return null;
|
||||
}
|
||||
@@ -849,6 +872,15 @@ public final class ApplicationExitInfo implements Parcelable {
|
||||
mAppTraceRetriever = retriever;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see mNativeTombstoneRetriever
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public void setNativeTombstoneRetriever(final IParcelFileDescriptorRetriever retriever) {
|
||||
mNativeTombstoneRetriever = retriever;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
@@ -878,6 +910,12 @@ public final class ApplicationExitInfo implements Parcelable {
|
||||
} else {
|
||||
dest.writeInt(0);
|
||||
}
|
||||
if (mNativeTombstoneRetriever != null) {
|
||||
dest.writeInt(1);
|
||||
dest.writeStrongBinder(mNativeTombstoneRetriever.asBinder());
|
||||
} else {
|
||||
dest.writeInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@@ -906,6 +944,7 @@ public final class ApplicationExitInfo implements Parcelable {
|
||||
mState = other.mState;
|
||||
mTraceFile = other.mTraceFile;
|
||||
mAppTraceRetriever = other.mAppTraceRetriever;
|
||||
mNativeTombstoneRetriever = other.mNativeTombstoneRetriever;
|
||||
}
|
||||
|
||||
private ApplicationExitInfo(@NonNull Parcel in) {
|
||||
@@ -928,6 +967,10 @@ public final class ApplicationExitInfo implements Parcelable {
|
||||
if (in.readInt() == 1) {
|
||||
mAppTraceRetriever = IAppTraceRetriever.Stub.asInterface(in.readStrongBinder());
|
||||
}
|
||||
if (in.readInt() == 1) {
|
||||
mNativeTombstoneRetriever = IParcelFileDescriptorRetriever.Stub.asInterface(
|
||||
in.readStrongBinder());
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull static final Creator<ApplicationExitInfo> CREATOR =
|
||||
@@ -986,6 +1029,7 @@ public final class ApplicationExitInfo implements Parcelable {
|
||||
sb.append(" state=").append(ArrayUtils.isEmpty(mState)
|
||||
? "empty" : Integer.toString(mState.length) + " bytes");
|
||||
sb.append(" trace=").append(mTraceFile);
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
31
core/java/android/app/IParcelFileDescriptorRetriever.aidl
Normal file
31
core/java/android/app/IParcelFileDescriptorRetriever.aidl
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.app;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
/**
|
||||
* An interface used to lazily provide a ParcelFileDescriptor to apps.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
interface IParcelFileDescriptorRetriever {
|
||||
/**
|
||||
* Retrieve the ParcelFileDescriptor.
|
||||
*/
|
||||
ParcelFileDescriptor getPfd();
|
||||
}
|
||||
@@ -364,6 +364,7 @@ import com.android.server.compat.PlatformCompat;
|
||||
import com.android.server.contentcapture.ContentCaptureManagerInternal;
|
||||
import com.android.server.firewall.IntentFirewall;
|
||||
import com.android.server.job.JobSchedulerInternal;
|
||||
import com.android.server.os.NativeTombstoneManager;
|
||||
import com.android.server.pm.Installer;
|
||||
import com.android.server.pm.permission.PermissionManagerServiceInternal;
|
||||
import com.android.server.uri.GrantUri;
|
||||
@@ -10410,6 +10411,9 @@ public class ActivityManagerService extends IActivityManager.Stub
|
||||
mUserController.handleIncomingUser(callingPid, callingUid, userId, true, ALLOW_NON_FULL,
|
||||
"getHistoricalProcessExitReasons", null);
|
||||
|
||||
NativeTombstoneManager tombstoneService = LocalServices.getService(
|
||||
NativeTombstoneManager.class);
|
||||
|
||||
final ArrayList<ApplicationExitInfo> results = new ArrayList<ApplicationExitInfo>();
|
||||
if (!TextUtils.isEmpty(packageName)) {
|
||||
final int uid = enforceDumpPermissionForPackage(packageName, userId, callingUid,
|
||||
@@ -10417,11 +10421,13 @@ public class ActivityManagerService extends IActivityManager.Stub
|
||||
if (uid != Process.INVALID_UID) {
|
||||
mProcessList.mAppExitInfoTracker.getExitInfo(
|
||||
packageName, uid, pid, maxNum, results);
|
||||
tombstoneService.collectTombstones(results, uid, pid, maxNum);
|
||||
}
|
||||
} else {
|
||||
// If no package name is given, use the caller's uid as the filter uid.
|
||||
mProcessList.mAppExitInfoTracker.getExitInfo(
|
||||
packageName, callingUid, pid, maxNum, results);
|
||||
tombstoneService.collectTombstones(results, callingUid, pid, maxNum);
|
||||
}
|
||||
|
||||
return new ParceledListSlice<ApplicationExitInfo>(results);
|
||||
|
||||
@@ -63,8 +63,10 @@ import com.android.internal.app.ProcessMap;
|
||||
import com.android.internal.util.ArrayUtils;
|
||||
import com.android.internal.util.function.pooled.PooledLambda;
|
||||
import com.android.server.IoThread;
|
||||
import com.android.server.LocalServices;
|
||||
import com.android.server.ServiceThread;
|
||||
import com.android.server.SystemServiceManager;
|
||||
import com.android.server.os.NativeTombstoneManager;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
@@ -78,6 +80,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
@@ -770,6 +773,10 @@ public final class AppExitInfoTracker {
|
||||
* Helper function for shell command
|
||||
*/
|
||||
void clearHistoryProcessExitInfo(String packageName, int userId) {
|
||||
NativeTombstoneManager tombstoneService = LocalServices.getService(
|
||||
NativeTombstoneManager.class);
|
||||
Optional<Integer> appId = Optional.empty();
|
||||
|
||||
if (TextUtils.isEmpty(packageName)) {
|
||||
synchronized (mLock) {
|
||||
removeByUserIdLocked(userId);
|
||||
@@ -777,10 +784,13 @@ public final class AppExitInfoTracker {
|
||||
} else {
|
||||
final int uid = mService.mPackageManagerInt.getPackageUid(packageName,
|
||||
PackageManager.MATCH_ALL, userId);
|
||||
appId = Optional.of(UserHandle.getAppId(uid));
|
||||
synchronized (mLock) {
|
||||
removePackageLocked(packageName, uid, true, userId);
|
||||
}
|
||||
}
|
||||
|
||||
tombstoneService.purge(Optional.of(userId), appId);
|
||||
schedulePersistProcessExitInfo(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,18 @@
|
||||
|
||||
package com.android.server.os;
|
||||
|
||||
import static android.app.ApplicationExitInfo.REASON_CRASH_NATIVE;
|
||||
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
|
||||
import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
|
||||
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
|
||||
|
||||
import android.annotation.AppIdInt;
|
||||
import android.annotation.CurrentTimeMillisLong;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.UserIdInt;
|
||||
import android.app.ActivityManager.RunningAppProcessInfo;
|
||||
import android.app.ApplicationExitInfo;
|
||||
import android.app.IParcelFileDescriptorRetriever;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -32,6 +38,7 @@ import android.os.ParcelFileDescriptor;
|
||||
import android.os.UserHandle;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.StructStat;
|
||||
import android.util.Slog;
|
||||
import android.util.SparseArray;
|
||||
import android.util.proto.ProtoInputStream;
|
||||
@@ -39,6 +46,7 @@ import android.util.proto.ProtoInputStream;
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.server.BootReceiver;
|
||||
import com.android.server.ServiceThread;
|
||||
import com.android.server.os.TombstoneProtos.Cause;
|
||||
import com.android.server.os.TombstoneProtos.Tombstone;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
@@ -47,7 +55,11 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* A class to manage native tombstones.
|
||||
@@ -153,7 +165,13 @@ public final class NativeTombstoneManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void purge(Optional<Integer> userId, Optional<Integer> appId) {
|
||||
/**
|
||||
* Remove native tombstones matching a user and/or app.
|
||||
*
|
||||
* @param userId user id to filter by, selects all users if empty
|
||||
* @param appId app id to filter by, selects all users if empty
|
||||
*/
|
||||
public void purge(Optional<Integer> userId, Optional<Integer> appId) {
|
||||
mHandler.post(() -> {
|
||||
synchronized (mLock) {
|
||||
for (int i = mTombstones.size() - 1; i >= 0; --i) {
|
||||
@@ -214,18 +232,97 @@ public final class NativeTombstoneManager {
|
||||
}, filter, null, mHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect native tombstones.
|
||||
*
|
||||
* @param output list to append to
|
||||
* @param callingUid POSIX uid to filter by
|
||||
* @param pid pid to filter by, ignored if zero
|
||||
* @param maxNum maximum number of elements in output
|
||||
*/
|
||||
public void collectTombstones(ArrayList<ApplicationExitInfo> output, int callingUid, int pid,
|
||||
int maxNum) {
|
||||
CompletableFuture<Object> future = new CompletableFuture<>();
|
||||
|
||||
if (!UserHandle.isApp(callingUid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int userId = UserHandle.getUserId(callingUid);
|
||||
final int appId = UserHandle.getAppId(callingUid);
|
||||
|
||||
mHandler.post(() -> {
|
||||
boolean appendedTombstones = false;
|
||||
|
||||
synchronized (mLock) {
|
||||
final int tombstonesSize = mTombstones.size();
|
||||
|
||||
tombstoneIter:
|
||||
for (int i = 0; i < tombstonesSize; ++i) {
|
||||
TombstoneFile tombstone = mTombstones.valueAt(i);
|
||||
if (tombstone.matches(Optional.of(userId), Optional.of(appId))) {
|
||||
if (pid != 0 && tombstone.mPid != pid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to attach to an existing REASON_CRASH_NATIVE.
|
||||
final int outputSize = output.size();
|
||||
for (int j = 0; j < outputSize; ++j) {
|
||||
ApplicationExitInfo exitInfo = output.get(j);
|
||||
if (tombstone.matches(exitInfo)) {
|
||||
exitInfo.setNativeTombstoneRetriever(tombstone.getPfdRetriever());
|
||||
continue tombstoneIter;
|
||||
}
|
||||
}
|
||||
|
||||
if (output.size() < maxNum) {
|
||||
appendedTombstones = true;
|
||||
output.add(tombstone.toAppExitInfo());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (appendedTombstones) {
|
||||
Collections.sort(output, (lhs, rhs) -> {
|
||||
// Reports should be ordered with newest reports first.
|
||||
long diff = rhs.getTimestamp() - lhs.getTimestamp();
|
||||
if (diff < 0) {
|
||||
return -1;
|
||||
} else if (diff == 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
future.complete(null);
|
||||
});
|
||||
|
||||
try {
|
||||
future.get();
|
||||
} catch (ExecutionException | InterruptedException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static class TombstoneFile {
|
||||
final ParcelFileDescriptor mPfd;
|
||||
|
||||
final @UserIdInt int mUserId;
|
||||
final @AppIdInt int mAppId;
|
||||
@UserIdInt int mUserId;
|
||||
@AppIdInt int mAppId;
|
||||
|
||||
int mPid;
|
||||
int mUid;
|
||||
String mProcessName;
|
||||
@CurrentTimeMillisLong long mTimestampMs;
|
||||
String mCrashReason;
|
||||
|
||||
boolean mPurged = false;
|
||||
final IParcelFileDescriptorRetriever mRetriever = new ParcelFileDescriptorRetriever();
|
||||
|
||||
TombstoneFile(ParcelFileDescriptor pfd, @UserIdInt int userId, @AppIdInt int appId) {
|
||||
TombstoneFile(ParcelFileDescriptor pfd) {
|
||||
mPfd = pfd;
|
||||
mUserId = userId;
|
||||
mAppId = appId;
|
||||
}
|
||||
|
||||
public boolean matches(Optional<Integer> userId, Optional<Integer> appId) {
|
||||
@@ -244,6 +341,26 @@ public final class NativeTombstoneManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean matches(ApplicationExitInfo exitInfo) {
|
||||
if (exitInfo.getReason() != REASON_CRASH_NATIVE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exitInfo.getPid() != mPid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exitInfo.getRealUid() != mUid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Math.abs(exitInfo.getTimestamp() - mTimestampMs) > 1000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
IoUtils.closeQuietly(mPfd);
|
||||
}
|
||||
@@ -271,16 +388,43 @@ public final class NativeTombstoneManager {
|
||||
final FileInputStream is = new FileInputStream(pfd.getFileDescriptor());
|
||||
final ProtoInputStream stream = new ProtoInputStream(is);
|
||||
|
||||
int pid = 0;
|
||||
int uid = 0;
|
||||
String processName = "";
|
||||
String crashReason = "";
|
||||
String selinuxLabel = "";
|
||||
|
||||
try {
|
||||
while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
|
||||
switch (stream.getFieldNumber()) {
|
||||
case (int) Tombstone.PID:
|
||||
pid = stream.readInt(Tombstone.PID);
|
||||
break;
|
||||
|
||||
case (int) Tombstone.UID:
|
||||
uid = stream.readInt(Tombstone.UID);
|
||||
break;
|
||||
|
||||
case (int) Tombstone.PROCESS_NAME:
|
||||
processName = stream.readString(Tombstone.PROCESS_NAME);
|
||||
break;
|
||||
|
||||
case (int) Tombstone.CAUSE:
|
||||
long token = stream.start(Tombstone.CAUSE);
|
||||
cause:
|
||||
while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
|
||||
switch (stream.getFieldNumber()) {
|
||||
case (int) Cause.HUMAN_READABLE:
|
||||
crashReason = stream.readString(Cause.HUMAN_READABLE);
|
||||
break cause;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
stream.end(token);
|
||||
|
||||
|
||||
case (int) Tombstone.SELINUX_LABEL:
|
||||
selinuxLabel = stream.readString(Tombstone.SELINUX_LABEL);
|
||||
break;
|
||||
@@ -299,6 +443,14 @@ public final class NativeTombstoneManager {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
long timestampMs = 0;
|
||||
try {
|
||||
StructStat stat = Os.fstat(pfd.getFileDescriptor());
|
||||
timestampMs = stat.st_atim.tv_sec * 1000 + stat.st_atim.tv_nsec / 1000000;
|
||||
} catch (ErrnoException ex) {
|
||||
Slog.e(TAG, "Failed to get timestamp of tombstone", ex);
|
||||
}
|
||||
|
||||
final int userId = UserHandle.getUserId(uid);
|
||||
final int appId = UserHandle.getAppId(uid);
|
||||
|
||||
@@ -307,7 +459,74 @@ public final class NativeTombstoneManager {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(new TombstoneFile(pfd, userId, appId));
|
||||
TombstoneFile result = new TombstoneFile(pfd);
|
||||
|
||||
result.mUserId = userId;
|
||||
result.mAppId = appId;
|
||||
result.mPid = pid;
|
||||
result.mUid = uid;
|
||||
result.mProcessName = processName;
|
||||
result.mTimestampMs = timestampMs;
|
||||
result.mCrashReason = crashReason;
|
||||
|
||||
return Optional.of(result);
|
||||
}
|
||||
|
||||
public IParcelFileDescriptorRetriever getPfdRetriever() {
|
||||
return mRetriever;
|
||||
}
|
||||
|
||||
public ApplicationExitInfo toAppExitInfo() {
|
||||
ApplicationExitInfo info = new ApplicationExitInfo();
|
||||
info.setPid(mPid);
|
||||
info.setRealUid(mUid);
|
||||
info.setPackageUid(mUid);
|
||||
info.setDefiningUid(mUid);
|
||||
info.setProcessName(mProcessName);
|
||||
info.setReason(ApplicationExitInfo.REASON_CRASH_NATIVE);
|
||||
|
||||
// Signal numbers are architecture-specific!
|
||||
// We choose to provide nothing here, to avoid leading users astray.
|
||||
info.setStatus(0);
|
||||
|
||||
// No way for us to find out.
|
||||
info.setImportance(RunningAppProcessInfo.IMPORTANCE_GONE);
|
||||
info.setPackageName("");
|
||||
info.setProcessStateSummary(null);
|
||||
|
||||
// We could find out, but they didn't get OOM-killed...
|
||||
info.setPss(0);
|
||||
info.setRss(0);
|
||||
|
||||
info.setTimestamp(mTimestampMs);
|
||||
info.setDescription(mCrashReason);
|
||||
|
||||
info.setSubReason(ApplicationExitInfo.SUBREASON_UNKNOWN);
|
||||
info.setNativeTombstoneRetriever(mRetriever);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
|
||||
class ParcelFileDescriptorRetriever extends IParcelFileDescriptorRetriever.Stub {
|
||||
ParcelFileDescriptorRetriever() {}
|
||||
|
||||
public @Nullable ParcelFileDescriptor getPfd() {
|
||||
if (mPurged) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reopen the file descriptor as read-only.
|
||||
try {
|
||||
final String path = "/proc/self/fd/" + mPfd.getFd();
|
||||
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(new File(path),
|
||||
MODE_READ_ONLY);
|
||||
return pfd;
|
||||
} catch (FileNotFoundException ex) {
|
||||
Slog.e(TAG, "failed to reopen file descriptor as read-only", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user