ask for permissions before opening restore backup. use insert or ignore for messages

This commit is contained in:
Daniel Gultsch 2019-01-23 11:20:36 +01:00
parent c9fc40dfe5
commit 18982174ce
6 changed files with 237 additions and 157 deletions

View file

@ -5,6 +5,7 @@ import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Binder;
import android.os.IBinder;
@ -20,13 +21,13 @@ import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.GZIPInputStream;
import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
@ -41,6 +42,7 @@ import eu.siacs.conversations.ui.ManageAccountActivity;
import eu.siacs.conversations.utils.BackupFileHeader;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import rocks.xmpp.addr.Jid;
import static eu.siacs.conversations.services.ExportBackupService.CIPHERMODE;
import static eu.siacs.conversations.services.ExportBackupService.KEYTYPE;
@ -49,13 +51,10 @@ import static eu.siacs.conversations.services.ExportBackupService.PROVIDER;
public class ImportBackupService extends Service {
private static final int NOTIFICATION_ID = 21;
private static AtomicBoolean running = new AtomicBoolean(false);
private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder();
private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName());
private final Set<OnBackupProcessed> mOnBackupProcessedListeners = Collections.newSetFromMap(new WeakHashMap<>());
private static AtomicBoolean running = new AtomicBoolean(false);
private DatabaseBackend mDatabaseBackend;
private NotificationManager notificationManager;
@ -85,7 +84,6 @@ public class ImportBackupService extends Service {
if (password == null || file == null) {
return START_NOT_STICKY;
}
Log.d(Config.LOGTAG, "on start command");
if (running.compareAndSet(false, true)) {
executor.execute(() -> {
startForegroundService();
@ -106,7 +104,8 @@ public class ImportBackupService extends Service {
public void loadBackupFiles(OnBackupFilesLoaded onBackupFilesLoaded) {
executor.execute(() -> {
final ArrayList<BackupFile> backupFiles = new ArrayList<>();
for (String app : Arrays.asList("Conversations", "Quicksy")) {
final Set<String> apps = new HashSet<>(Arrays.asList("Conversations", "Quicksy", getString(R.string.app_name)));
for (String app : apps) {
final File directory = new File(FileBackend.getBackupDirectory(app));
if (!directory.exists() || !directory.isDirectory()) {
Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath());
@ -154,9 +153,11 @@ public class ImportBackupService extends Service {
BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, "UTF-8"));
String line;
StringBuilder multiLineQuery = null;
int error = 0;
while ((line = reader.readLine()) != null) {
int count = count(line, '\'');
if (multiLineQuery != null) {
multiLineQuery.append('\n');
multiLineQuery.append(line);
if (count % 2 == 1) {
db.execSQL(multiLineQuery.toString());
@ -171,6 +172,12 @@ public class ImportBackupService extends Service {
}
}
Log.d(Config.LOGTAG, "done reading file");
final Jid jid = backupFileHeader.getJid();
Cursor countCursor = db.rawQuery("select count(messages.uuid) from messages join conversations on conversations.uuid=messages.conversationUuid join accounts on conversations.accountUuid=accounts.uuid where accounts.username=? and accounts.server=?", new String[]{jid.getEscapedLocal(), jid.getDomain()});
countCursor.moveToFirst();
int count = countCursor.getInt(0);
Log.d(Config.LOGTAG, "restored " + count + " messages");
countCursor.close();
stopBackgroundService();
synchronized (mOnBackupProcessedListeners) {
for (OnBackupProcessed l : mOnBackupProcessedListeners) {
@ -207,7 +214,7 @@ public class ImportBackupService extends Service {
.setAutoCancel(true)
.setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), PendingIntent.FLAG_UPDATE_CURRENT))
.setSmallIcon(R.drawable.ic_unarchive_white_24dp);
notificationManager.notify(NOTIFICATION_ID,mBuilder.build());
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
}
private void stopBackgroundService() {
@ -232,6 +239,18 @@ public class ImportBackupService extends Service {
return this.binder;
}
public interface OnBackupFilesLoaded {
void onBackupFilesLoaded(List<BackupFile> files);
}
public interface OnBackupProcessed {
void onBackupRestored();
void onBackupDecryptionFailed();
void onBackupRestoreFailed();
}
public static class BackupFile {
private final File file;
private final BackupFileHeader header;
@ -263,14 +282,4 @@ public class ImportBackupService extends Service {
return ImportBackupService.this;
}
}
public interface OnBackupFilesLoaded {
void onBackupFilesLoaded(List<BackupFile> files);
}
public interface OnBackupProcessed {
void onBackupRestored();
void onBackupDecryptionFailed();
void onBackupRestoreFailed();
}
}

View file

@ -5,6 +5,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.support.annotation.NonNull;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.util.Pair;
@ -35,10 +36,15 @@ import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
import eu.siacs.conversations.xmpp.XmppConnection;
import rocks.xmpp.addr.Jid;
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated, AccountAdapter.OnTglAccountState {
private final String STATE_SELECTED_ACCOUNT = "selected_account";
private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
protected Account selectedAccount = null;
protected Jid selectedAccountJid = null;
@ -201,7 +207,9 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
startActivity(new Intent(this, EditAccountActivity.class));
break;
case R.id.action_import_backup:
if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
startActivity(new Intent(this, ImportBackupActivity.class));
}
break;
case R.id.action_disable_all:
disableAllAccounts();
@ -218,6 +226,27 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
return super.onOptionsItemSelected(item);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
if (grantResults.length > 0) {
if (allGranted(grantResults)) {
switch (requestCode) {
case REQUEST_IMPORT_BACKUP:
startActivity(new Intent(this, ImportBackupActivity.class));
break;
}
} else {
Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
}
}
if (writeGranted(grantResults, permissions)) {
if (xmppConnectionService != null) {
xmppConnectionService.restartFileObserver();
}
}
}
@Override
public boolean onNavigateUp() {
if (xmppConnectionService.getConversations().size() == 0) {

View file

@ -3,22 +3,26 @@ package eu.siacs.conversations.ui;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.support.annotation.NonNull;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Button;
import android.widget.Toast;
import java.util.List;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.ImportBackupService;
import eu.siacs.conversations.utils.XmppUri;
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
public class WelcomeActivity extends XmppActivity {
private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
@Override
protected void refreshUiReal() {
@ -90,12 +94,34 @@ public class WelcomeActivity extends XmppActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_import_backup) {
if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
startActivity(new Intent(this, ImportBackupActivity.class));
}
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
if (grantResults.length > 0) {
if (allGranted(grantResults)) {
switch (requestCode) {
case REQUEST_IMPORT_BACKUP:
startActivity(new Intent(this, ImportBackupActivity.class));
break;
}
} else {
Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
}
}
if (writeGranted(grantResults, permissions)) {
if (xmppConnectionService != null) {
xmppConnectionService.restartFileObserver();
}
}
}
public void addInviteUri(Intent intent) {
StartConversationActivity.addInviteUri(intent, getIntent());
}

View file

@ -49,33 +49,14 @@ public class ExportBackupService extends Service {
public static final String PROVIDER = "BC";
private static final int NOTIFICATION_ID = 19;
private static final int PAGE_SIZE = 20;
private static AtomicBoolean running = new AtomicBoolean(false);
private DatabaseBackend mDatabaseBackend;
private List<Account> mAccounts;
private NotificationManager notificationManager;
@Override
public void onCreate() {
mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
mAccounts = mDatabaseBackend.getAccounts();
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (running.compareAndSet(false, true)) {
new Thread(() -> {
export();
stopForeground(true);
running.set(false);
stopSelf();
}).start();
}
return START_NOT_STICKY;
}
private static void accountExport(SQLiteDatabase db, String uuid, PrintWriter writer) {
StringBuilder builder = new StringBuilder();
final StringBuilder builder = new StringBuilder();
final Cursor accountCursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", new String[]{uuid}, null, null, null);
while (accountCursor != null && accountCursor.moveToNext()) {
builder.append("INSERT INTO ").append(Account.TABLENAME).append("(");
@ -95,10 +76,8 @@ public class ExportBackupService extends Service {
builder.append("NULL");
} else if (value.matches("\\d+")) {
int intValue = Integer.parseInt(value);
Log.d(Config.LOGTAG,"reading int value. "+intValue);
if (Account.OPTIONS.equals(accountCursor.getColumnName(i))) {
intValue |= 1 << Account.OPTION_DISABLED;
Log.d(Config.LOGTAG,"modified int value "+intValue);
}
builder.append(intValue);
} else {
@ -109,102 +88,22 @@ public class ExportBackupService extends Service {
builder.append(';');
builder.append('\n');
}
Log.d(Config.LOGTAG,builder.toString());
if (accountCursor != null) {
accountCursor.close();
}
writer.append(builder.toString());
}
private void messageExport(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) {
Cursor cursor = db.rawQuery("select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid});
int size = cursor != null ? cursor.getCount() : 0;
Log.d(Config.LOGTAG, "exporting " + size + " messages");
int i = 0;
int p = 0;
while (cursor != null && cursor.moveToNext()) {
writer.write(cursorToString(Message.TABLENAME, cursor, 20));
if (i + 20 > size) {
i = size;
} else {
i += 20;
}
final int percentage = i * 100 / size;
if (p < percentage) {
p = percentage;
notificationManager.notify(NOTIFICATION_ID,progress.build(p));
Log.d(Config.LOGTAG, "percentage=" + p);
}
}
if (cursor != null) {
cursor.close();
}
}
private static void simpleExport(SQLiteDatabase db, String table, String column, String uuid, PrintWriter writer) {
final Cursor cursor = db.query(table, null, column + "=?", new String[]{uuid}, null, null, null);
while (cursor != null && cursor.moveToNext()) {
writer.write(cursorToString(table, cursor, 20));
writer.write(cursorToString(table, cursor, PAGE_SIZE));
}
if (cursor != null) {
cursor.close();
}
}
private void export() {
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
mBuilder.setContentTitle(getString(R.string.notification_create_backup_title))
.setSmallIcon(R.drawable.ic_archive_white_24dp)
.setProgress(1, 0, false);
startForeground(NOTIFICATION_ID, mBuilder.build());
try {
int count = 0;
final int max = this.mAccounts.size();
final SecureRandom secureRandom = new SecureRandom();
for (Account account : this.mAccounts) {
final byte[] IV = new byte[12];
final byte[] salt = new byte[16];
secureRandom.nextBytes(IV);
secureRandom.nextBytes(salt);
final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name),account.getJid(),System.currentTimeMillis(),IV,salt);
final Progress progress = new Progress(mBuilder, max, count);
final File file = new File(FileBackend.getBackupDirectory(this)+account.getJid().asBareJid().toEscapedString()+".ceb");
if (file.getParentFile().mkdirs()) {
Log.d(Config.LOGTAG,"created backup directory "+file.getParentFile().getAbsolutePath());
}
final FileOutputStream fileOutputStream = new FileOutputStream(file);
final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
backupFileHeader.write(dataOutputStream);
dataOutputStream.flush();
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
byte[] key = getKey(account.getPassword(), salt);
Log.d(Config.LOGTAG,backupFileHeader.toString());
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
PrintWriter writer = new PrintWriter(gzipOutputStream);
SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
final String uuid = account.getUuid();
accountExport(db, uuid, writer);
simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer);
messageExport(db, uuid, writer, progress);
for(String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT,uuid,writer);
}
writer.flush();
writer.close();
Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
count++;
}
} catch (Exception e) {
Log.d(Config.LOGTAG, "unable to create backup ", e);
}
}
public static byte[] getKey(String password, byte[] salt) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
@ -215,8 +114,16 @@ public class ExportBackupService extends Service {
}
private static String cursorToString(String tablename, Cursor cursor, int max) {
return cursorToString(tablename, cursor, max, false);
}
private static String cursorToString(String tablename, Cursor cursor, int max, boolean ignore) {
StringBuilder builder = new StringBuilder();
builder.append("INSERT INTO ").append(tablename).append("(");
builder.append("INSERT ");
if (ignore) {
builder.append("OR IGNORE ");
}
builder.append("INTO ").append(tablename).append("(");
for (int i = 0; i < cursor.getColumnCount(); ++i) {
if (i != 0) {
builder.append(',');
@ -229,7 +136,7 @@ public class ExportBackupService extends Service {
builder.append(',');
}
appendValues(cursor, builder);
if (!cursor.moveToNext()) {
if (i < max - 1 && !cursor.moveToNext()) {
break;
}
}
@ -257,6 +164,105 @@ public class ExportBackupService extends Service {
}
@Override
public void onCreate() {
mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
mAccounts = mDatabaseBackend.getAccounts();
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (running.compareAndSet(false, true)) {
new Thread(() -> {
export();
stopForeground(true);
running.set(false);
stopSelf();
}).start();
return START_STICKY;
}
return START_NOT_STICKY;
}
private void messageExport(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) {
Cursor cursor = db.rawQuery("select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid});
int size = cursor != null ? cursor.getCount() : 0;
Log.d(Config.LOGTAG, "exporting " + size + " messages");
int i = 0;
int p = 0;
while (cursor != null && cursor.moveToNext()) {
writer.write(cursorToString(Message.TABLENAME, cursor, PAGE_SIZE, false));
if (i + PAGE_SIZE > size) {
i = size;
} else {
i += PAGE_SIZE;
}
final int percentage = i * 100 / size;
if (p < percentage) {
p = percentage;
notificationManager.notify(NOTIFICATION_ID, progress.build(p));
}
}
if (cursor != null) {
cursor.close();
}
}
private void export() {
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
mBuilder.setContentTitle(getString(R.string.notification_create_backup_title))
.setSmallIcon(R.drawable.ic_archive_white_24dp)
.setProgress(1, 0, false);
startForeground(NOTIFICATION_ID, mBuilder.build());
try {
int count = 0;
final int max = this.mAccounts.size();
final SecureRandom secureRandom = new SecureRandom();
for (Account account : this.mAccounts) {
final byte[] IV = new byte[12];
final byte[] salt = new byte[16];
secureRandom.nextBytes(IV);
secureRandom.nextBytes(salt);
final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt);
final Progress progress = new Progress(mBuilder, max, count);
final File file = new File(FileBackend.getBackupDirectory(this) + account.getJid().asBareJid().toEscapedString() + ".ceb");
if (file.getParentFile().mkdirs()) {
Log.d(Config.LOGTAG, "created backup directory " + file.getParentFile().getAbsolutePath());
}
final FileOutputStream fileOutputStream = new FileOutputStream(file);
final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
backupFileHeader.write(dataOutputStream);
dataOutputStream.flush();
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
byte[] key = getKey(account.getPassword(), salt);
Log.d(Config.LOGTAG, backupFileHeader.toString());
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
PrintWriter writer = new PrintWriter(gzipOutputStream);
SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
final String uuid = account.getUuid();
accountExport(db, uuid, writer);
simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer);
messageExport(db, uuid, writer, progress);
for (String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer);
}
writer.flush();
writer.close();
Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
count++;
}
} catch (Exception e) {
Log.d(Config.LOGTAG, "unable to create backup ", e);
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
@ -274,7 +280,7 @@ public class ExportBackupService extends Service {
}
private Notification build(int percentage) {
builder.setProgress(max * 100,count * 100 + percentage,false);
builder.setProgress(max * 100, count * 100 + percentage, false);
return builder.build();
}
}

View file

@ -119,6 +119,9 @@ import rocks.xmpp.addr.Jid;
import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT;
import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION;
import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard;
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureClicked {
@ -523,33 +526,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
return getConversation(activity, R.id.main_fragment);
}
private static boolean allGranted(int[] grantResults) {
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
private static boolean writeGranted(int[] grantResults, String[] permission) {
for (int i = 0; i < grantResults.length; ++i) {
if (Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permission[i])) {
return grantResults[i] == PackageManager.PERMISSION_GRANTED;
}
}
return false;
}
private static String getFirstDenied(int[] grantResults, String[] permissions) {
for (int i = 0; i < grantResults.length; ++i) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
return permissions[i];
}
}
return null;
}
private static boolean scrolledToBottom(AbsListView listView) {
final int count = listView.getCount();
if (count == 0) {

View file

@ -0,0 +1,34 @@
package eu.siacs.conversations.utils;
import android.Manifest;
import android.content.pm.PackageManager;
public class PermissionUtils {
public static boolean allGranted(int[] grantResults) {
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
public static boolean writeGranted(int[] grantResults, String[] permission) {
for (int i = 0; i < grantResults.length; ++i) {
if (Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permission[i])) {
return grantResults[i] == PackageManager.PERMISSION_GRANTED;
}
}
return false;
}
public static String getFirstDenied(int[] grantResults, String[] permissions) {
for (int i = 0; i < grantResults.length; ++i) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
return permissions[i];
}
}
return null;
}
}