Merge branch 'master' of github.com:open-keychain/open-keychain
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
package org.spongycastle.openpgp.jcajce;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.spongycastle.openpgp.PGPMarker;
|
||||
|
||||
/** This class wraps the regular PGPObjectFactory, changing its behavior to
|
||||
* ignore all PGPMarker packets it encounters while reading. These packets
|
||||
* carry no semantics of their own, and should be ignored according to
|
||||
* RFC 4880.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc4880#section-5.8
|
||||
* @see org.spongycastle.openpgp.PGPMarker
|
||||
*
|
||||
*/
|
||||
public class JcaSkipMarkerPGPObjectFactory extends JcaPGPObjectFactory {
|
||||
|
||||
public JcaSkipMarkerPGPObjectFactory(InputStream in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object nextObject() throws IOException {
|
||||
Object o = super.nextObject();
|
||||
while (o instanceof PGPMarker) {
|
||||
o = super.nextObject();
|
||||
}
|
||||
return o;
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ public final class Constants {
|
||||
|
||||
public static final boolean DEBUG = BuildConfig.DEBUG;
|
||||
public static final boolean DEBUG_LOG_DB_QUERIES = false;
|
||||
public static final boolean DEBUG_EXPLAIN_QUERIES = false;
|
||||
public static final boolean DEBUG_SYNC_REMOVE_CONTACTS = false;
|
||||
public static final boolean DEBUG_KEYSERVER_SYNC = false;
|
||||
|
||||
|
||||
@@ -110,10 +110,9 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> {
|
||||
if (decryptResult.isPending()) {
|
||||
return new InputDataResult(log, decryptResult);
|
||||
}
|
||||
log.addByMerge(decryptResult, 2);
|
||||
log.addByMerge(decryptResult, 1);
|
||||
|
||||
if (!decryptResult.success()) {
|
||||
log.add(LogType.MSG_DATA_ERROR_OPENPGP, 1);
|
||||
if ( ! decryptResult.success()) {
|
||||
return new InputDataResult(InputDataResult.RESULT_ERROR, log);
|
||||
}
|
||||
|
||||
@@ -165,6 +164,7 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> {
|
||||
parser.setContentDecoding(true);
|
||||
parser.setRecurse();
|
||||
parser.setContentHandler(new AbstractContentHandler() {
|
||||
private boolean mFoundHeaderWithFields = false;
|
||||
private Uri uncheckedSignedDataUri;
|
||||
String mFilename;
|
||||
|
||||
@@ -220,12 +220,20 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> {
|
||||
mFilename = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endHeader() throws MimeException {
|
||||
if ( ! mFoundHeaderWithFields) {
|
||||
parser.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void field(Field field) throws MimeException {
|
||||
field = DefaultFieldParser.getParser().parse(field, DecodeMonitor.SILENT);
|
||||
if (field instanceof ContentDispositionField) {
|
||||
mFilename = ((ContentDispositionField) field).getFilename();
|
||||
}
|
||||
mFoundHeaderWithFields = true;
|
||||
}
|
||||
|
||||
private void bodySignature(BodyDescriptor bd, InputStream is) throws MimeException, IOException {
|
||||
|
||||
@@ -836,7 +836,6 @@ public abstract class OperationResult implements Parcelable {
|
||||
MSG_DATA (LogLevel.START, R.string.msg_data),
|
||||
MSG_DATA_OPENPGP (LogLevel.DEBUG, R.string.msg_data_openpgp),
|
||||
MSG_DATA_ERROR_IO (LogLevel.ERROR, R.string.msg_data_error_io),
|
||||
MSG_DATA_ERROR_OPENPGP (LogLevel.ERROR, R.string.msg_data_error_openpgp),
|
||||
MSG_DATA_DETACHED (LogLevel.INFO, R.string.msg_data_detached),
|
||||
MSG_DATA_DETACHED_CLEAR (LogLevel.WARN, R.string.msg_data_detached_clear),
|
||||
MSG_DATA_DETACHED_SIG (LogLevel.DEBUG, R.string.msg_data_detached_sig),
|
||||
|
||||
@@ -48,7 +48,7 @@ import org.spongycastle.openpgp.PGPPBEEncryptedData;
|
||||
import org.spongycastle.openpgp.PGPPublicKeyEncryptedData;
|
||||
import org.spongycastle.openpgp.PGPSignatureList;
|
||||
import org.spongycastle.openpgp.PGPUtil;
|
||||
import org.spongycastle.openpgp.jcajce.JcaPGPObjectFactory;
|
||||
import org.spongycastle.openpgp.jcajce.JcaSkipMarkerPGPObjectFactory;
|
||||
import org.spongycastle.openpgp.operator.PBEDataDecryptorFactory;
|
||||
import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider;
|
||||
import org.spongycastle.openpgp.operator.jcajce.CachingDataDecryptorFactory;
|
||||
@@ -281,11 +281,11 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
|
||||
OpenPgpDecryptionResultBuilder decryptionResultBuilder = new OpenPgpDecryptionResultBuilder();
|
||||
|
||||
JcaPGPObjectFactory plainFact;
|
||||
JcaSkipMarkerPGPObjectFactory plainFact;
|
||||
Object dataChunk;
|
||||
EncryptStreamResult esResult = null;
|
||||
{ // resolve encrypted (symmetric and asymmetric) packets
|
||||
JcaPGPObjectFactory pgpF = new JcaPGPObjectFactory(in);
|
||||
JcaSkipMarkerPGPObjectFactory pgpF = new JcaSkipMarkerPGPObjectFactory(in);
|
||||
Object obj = pgpF.nextObject();
|
||||
|
||||
if (obj instanceof PGPEncryptedDataList) {
|
||||
@@ -312,7 +312,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
decryptionResultBuilder.setInsecure(true);
|
||||
}
|
||||
|
||||
plainFact = new JcaPGPObjectFactory(esResult.cleartextStream);
|
||||
plainFact = new JcaSkipMarkerPGPObjectFactory(esResult.cleartextStream);
|
||||
dataChunk = plainFact.nextObject();
|
||||
|
||||
} else {
|
||||
@@ -337,7 +337,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
|
||||
PGPCompressedData compressedData = (PGPCompressedData) dataChunk;
|
||||
|
||||
JcaPGPObjectFactory fact = new JcaPGPObjectFactory(compressedData.getDataStream());
|
||||
JcaSkipMarkerPGPObjectFactory fact = new JcaSkipMarkerPGPObjectFactory(compressedData.getDataStream());
|
||||
dataChunk = fact.nextObject();
|
||||
plainFact = fact;
|
||||
}
|
||||
@@ -839,7 +839,7 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
}
|
||||
|
||||
updateProgress(R.string.progress_processing_signature, 60, 100);
|
||||
JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(aIn);
|
||||
JcaSkipMarkerPGPObjectFactory pgpFact = new JcaSkipMarkerPGPObjectFactory(aIn);
|
||||
|
||||
PgpSignatureChecker signatureChecker = new PgpSignatureChecker(mProviderHelper);
|
||||
|
||||
@@ -891,12 +891,12 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
InputStream detachedSigIn = new ByteArrayInputStream(input.getDetachedSignature());
|
||||
detachedSigIn = PGPUtil.getDecoderStream(detachedSigIn);
|
||||
|
||||
JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(detachedSigIn);
|
||||
JcaSkipMarkerPGPObjectFactory pgpFact = new JcaSkipMarkerPGPObjectFactory(detachedSigIn);
|
||||
|
||||
Object o = pgpFact.nextObject();
|
||||
if (o instanceof PGPCompressedData) {
|
||||
PGPCompressedData c1 = (PGPCompressedData) o;
|
||||
pgpFact = new JcaPGPObjectFactory(c1.getDataStream());
|
||||
pgpFact = new JcaSkipMarkerPGPObjectFactory(c1.getDataStream());
|
||||
o = pgpFact.nextObject();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,23 +18,15 @@
|
||||
|
||||
package org.sufficientlysecure.keychain.pgp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class PgpHelper {
|
||||
|
||||
@@ -50,35 +42,6 @@ public class PgpHelper {
|
||||
".*?(-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----).*",
|
||||
Pattern.DOTALL);
|
||||
|
||||
/**
|
||||
* Deletes file securely by overwriting it with random data before deleting it.
|
||||
* <p/>
|
||||
* TODO: Does this really help on flash storage?
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void deleteFileSecurely(Context context, Progressable progressable, File file)
|
||||
throws IOException {
|
||||
long length = file.length();
|
||||
SecureRandom random = new SecureRandom();
|
||||
RandomAccessFile raf = new RandomAccessFile(file, "rws");
|
||||
raf.seek(0);
|
||||
raf.getFilePointer();
|
||||
byte[] data = new byte[1 << 16];
|
||||
int pos = 0;
|
||||
String msg = context.getString(R.string.progress_deleting_securely, file.getName());
|
||||
while (pos < length) {
|
||||
if (progressable != null) {
|
||||
progressable.setProgress(msg, (int) (100 * pos / length), 100);
|
||||
}
|
||||
random.nextBytes(data);
|
||||
raf.write(data);
|
||||
pos += data.length;
|
||||
}
|
||||
raf.close();
|
||||
file.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixing broken PGP MESSAGE Strings coming from GMail/AOSP Mail
|
||||
*/
|
||||
|
||||
@@ -54,7 +54,7 @@ import java.io.IOException;
|
||||
*/
|
||||
public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
private static final String DATABASE_NAME = "openkeychain.db";
|
||||
private static final int DATABASE_VERSION = 13;
|
||||
private static final int DATABASE_VERSION = 14;
|
||||
static Boolean apgHack = false;
|
||||
private Context mContext;
|
||||
|
||||
@@ -79,7 +79,7 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
private static final String CREATE_KEYRINGS_SECRET =
|
||||
"CREATE TABLE IF NOT EXISTS keyrings_secret ("
|
||||
+ KeyRingsColumns.MASTER_KEY_ID + " INTEGER PRIMARY KEY,"
|
||||
+ KeyRingsColumns.KEY_RING_DATA + " BLOB,"
|
||||
+ KeyRingsColumns.KEY_RING_DATA + " BLOB, "
|
||||
+ "FOREIGN KEY(" + KeyRingsColumns.MASTER_KEY_ID + ") "
|
||||
+ "REFERENCES keyrings_public(" + KeyRingsColumns.MASTER_KEY_ID + ") ON DELETE CASCADE"
|
||||
+ ")";
|
||||
@@ -220,6 +220,13 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
db.execSQL(CREATE_API_APPS);
|
||||
db.execSQL(CREATE_API_APPS_ACCOUNTS);
|
||||
db.execSQL(CREATE_API_APPS_ALLOWED_KEYS);
|
||||
|
||||
db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ");");
|
||||
db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", "
|
||||
+ UserPacketsColumns.USER_ID + ", " + UserPacketsColumns.MASTER_KEY_ID + ");");
|
||||
db.execSQL("CREATE INDEX verified_certs ON certs ("
|
||||
+ CertsColumns.VERIFIED + ", " + CertsColumns.MASTER_KEY_ID + ");");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -291,13 +298,14 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
db.execSQL("DELETE FROM api_accounts WHERE key_id BETWEEN 0 AND 3");
|
||||
case 12:
|
||||
db.execSQL(CREATE_UPDATE_KEYS);
|
||||
if (oldVersion == 10) {
|
||||
// no consolidate if we are updating from 10, we're just here for
|
||||
// the api_accounts fix and the new update keys table
|
||||
return;
|
||||
}
|
||||
case 13:
|
||||
// do nothing here, just consolidate
|
||||
case 14:
|
||||
db.execSQL("CREATE INDEX keys_by_rank ON keys (" + KeysColumns.RANK + ");");
|
||||
db.execSQL("CREATE INDEX uids_by_rank ON user_packets (" + UserPacketsColumns.RANK + ", "
|
||||
+ UserPacketsColumns.USER_ID + ", " + UserPacketsColumns.MASTER_KEY_ID + ");");
|
||||
db.execSQL("CREATE INDEX verified_certs ON certs ("
|
||||
+ CertsColumns.VERIFIED + ", " + CertsColumns.MASTER_KEY_ID + ");");
|
||||
|
||||
}
|
||||
|
||||
@@ -310,6 +318,10 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
// Downgrade is ok for the debug version, makes it easier to work with branches
|
||||
if (Constants.DEBUG) {
|
||||
return;
|
||||
}
|
||||
// NOTE: downgrading the database is explicitly not allowed to prevent
|
||||
// someone from exploiting old bugs to export the database
|
||||
throw new RuntimeException("Downgrading the database is not allowed!");
|
||||
|
||||
@@ -303,14 +303,14 @@ public class KeychainProvider extends ContentProvider {
|
||||
projectionMap.put(KeyRings.FINGERPRINT, Tables.KEYS + "." + Keys.FINGERPRINT);
|
||||
projectionMap.put(KeyRings.USER_ID, Tables.USER_PACKETS + "." + UserPackets.USER_ID);
|
||||
projectionMap.put(KeyRings.HAS_DUPLICATE_USER_ID,
|
||||
"(SELECT COUNT (*) FROM " + Tables.USER_PACKETS + " AS dups"
|
||||
"(EXISTS (SELECT * FROM " + Tables.USER_PACKETS + " AS dups"
|
||||
+ " WHERE dups." + UserPackets.MASTER_KEY_ID
|
||||
+ " != " + Tables.KEYS + "." + Keys.MASTER_KEY_ID
|
||||
+ " AND dups." + UserPackets.RANK + " = 0"
|
||||
+ " AND dups." + UserPackets.USER_ID
|
||||
+ " = "+ Tables.USER_PACKETS + "." + UserPackets.USER_ID
|
||||
+ ") AS " + KeyRings.HAS_DUPLICATE_USER_ID);
|
||||
projectionMap.put(KeyRings.VERIFIED, KeyRings.VERIFIED);
|
||||
+ ")) AS " + KeyRings.HAS_DUPLICATE_USER_ID);
|
||||
projectionMap.put(KeyRings.VERIFIED, Tables.CERTS + "." + Certs.VERIFIED);
|
||||
projectionMap.put(KeyRings.PUBKEY_DATA,
|
||||
Tables.KEY_RINGS_PUBLIC + "." + KeyRingData.KEY_RING_DATA
|
||||
+ " AS " + KeyRings.PUBKEY_DATA);
|
||||
@@ -319,10 +319,8 @@ public class KeychainProvider extends ContentProvider {
|
||||
+ " AS " + KeyRings.PRIVKEY_DATA);
|
||||
projectionMap.put(KeyRings.HAS_SECRET, Tables.KEYS + "." + KeyRings.HAS_SECRET);
|
||||
projectionMap.put(KeyRings.HAS_ANY_SECRET,
|
||||
"(EXISTS (SELECT * FROM " + Tables.KEY_RINGS_SECRET
|
||||
+ " WHERE " + Tables.KEY_RINGS_SECRET + "." + KeyRingData.MASTER_KEY_ID
|
||||
+ " = " + Tables.KEYS + "." + Keys.MASTER_KEY_ID
|
||||
+ ")) AS " + KeyRings.HAS_ANY_SECRET);
|
||||
"(" + Tables.KEY_RINGS_SECRET + "." + KeyRings.MASTER_KEY_ID + " IS NOT NULL)" +
|
||||
" AS " + KeyRings.HAS_ANY_SECRET);
|
||||
projectionMap.put(KeyRings.HAS_ENCRYPT,
|
||||
"kE." + Keys.KEY_ID + " AS " + KeyRings.HAS_ENCRYPT);
|
||||
projectionMap.put(KeyRings.HAS_SIGN,
|
||||
@@ -363,7 +361,7 @@ public class KeychainProvider extends ContentProvider {
|
||||
+ " = "
|
||||
+ Tables.KEY_RINGS_PUBLIC + "." + KeyRingData.MASTER_KEY_ID
|
||||
+ ")" : "")
|
||||
+ (plist.contains(KeyRings.PRIVKEY_DATA) ?
|
||||
+ (plist.contains(KeyRings.PRIVKEY_DATA) || plist.contains(KeyRings.HAS_ANY_SECRET) ?
|
||||
" LEFT JOIN " + Tables.KEY_RINGS_SECRET + " ON ("
|
||||
+ Tables.KEYS + "." + Keys.MASTER_KEY_ID
|
||||
+ " = "
|
||||
@@ -712,20 +710,45 @@ public class KeychainProvider extends ContentProvider {
|
||||
}
|
||||
|
||||
SQLiteDatabase db = getDb().getReadableDatabase();
|
||||
|
||||
Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, having, orderBy);
|
||||
if (cursor != null) {
|
||||
// Tell the cursor what uri to watch, so it knows when its source data changes
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
}
|
||||
|
||||
Log.d(Constants.TAG,
|
||||
"Query: " + qb.buildQuery(projection, selection, null, null, orderBy, null));
|
||||
|
||||
if (Constants.DEBUG && Constants.DEBUG_LOG_DB_QUERIES) {
|
||||
Log.d(Constants.TAG,
|
||||
"Query: "
|
||||
+ qb.buildQuery(projection, selection, selectionArgs, null, null,
|
||||
orderBy, null));
|
||||
Log.d(Constants.TAG, "Cursor: " + DatabaseUtils.dumpCursorToString(cursor));
|
||||
}
|
||||
|
||||
if (Constants.DEBUG && Constants.DEBUG_EXPLAIN_QUERIES) {
|
||||
String rawQuery = qb.buildQuery(projection, selection, groupBy, having, orderBy, null);
|
||||
Cursor explainCursor = db.rawQuery("EXPLAIN QUERY PLAN " + rawQuery, selectionArgs);
|
||||
|
||||
// this is a debugging feature, we can be a little careless
|
||||
explainCursor.moveToFirst();
|
||||
|
||||
StringBuilder line = new StringBuilder();
|
||||
for (int i = 0; i < explainCursor.getColumnCount(); i++) {
|
||||
line.append(explainCursor.getColumnName(i)).append(", ");
|
||||
}
|
||||
Log.d(Constants.TAG, line.toString());
|
||||
|
||||
while (!explainCursor.isAfterLast()) {
|
||||
line = new StringBuilder();
|
||||
for (int i = 0; i < explainCursor.getColumnCount(); i++) {
|
||||
line.append(explainCursor.getString(i)).append(", ");
|
||||
}
|
||||
Log.d(Constants.TAG, line.toString());
|
||||
explainCursor.moveToNext();
|
||||
}
|
||||
|
||||
explainCursor.close();
|
||||
}
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,10 +73,14 @@ import org.openintents.openpgp.OpenPgpSignatureResult;
|
||||
import org.sufficientlysecure.keychain.BuildConfig;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
|
||||
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.InputDataResult;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.service.InputDataParcel;
|
||||
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
|
||||
import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment;
|
||||
// this import NEEDS to be above the ViewModel AND SubViewHolder one, or it won't compile! (as of 16.09.15)
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.StatusHolder;
|
||||
@@ -90,8 +94,25 @@ import org.sufficientlysecure.keychain.ui.util.Notify.Style;
|
||||
import org.sufficientlysecure.keychain.util.FileHelper;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.ParcelableHashMap;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
|
||||
|
||||
/** Displays a list of decrypted inputs.
|
||||
*
|
||||
* This class has a complex control flow to manage its input URIs. Each URI
|
||||
* which is in mInputUris is also in exactly one of mPendingInputUris,
|
||||
* mCancelledInputUris, mCurrentInputUri, or a key in mInputDataResults.
|
||||
*
|
||||
* Processing of URIs happens using a looping approach:
|
||||
* - There is always exactly one method running which works on mCurrentInputUri
|
||||
* - Processing starts in cryptoOperation(), which pops a new mCurrentInputUri
|
||||
* from the list of mPendingInputUris.
|
||||
* - Once a mCurrentInputUri is finished processing, it should be set to null and
|
||||
* control handed back to cryptoOperation()
|
||||
* - Control flow can move through asynchronous calls, and resume in callbacks
|
||||
* like onActivityResult() or onPermissionRequestResult().
|
||||
*
|
||||
*/
|
||||
public class DecryptListFragment
|
||||
extends QueueingCryptoOperationFragment<InputDataParcel,InputDataResult>
|
||||
implements OnMenuItemClickListener {
|
||||
@@ -103,7 +124,7 @@ public class DecryptListFragment
|
||||
public static final String ARG_CAN_DELETE = "can_delete";
|
||||
|
||||
private static final int REQUEST_CODE_OUTPUT = 0x00007007;
|
||||
private static final int MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 12;
|
||||
private static final int REQUEST_PERMISSION_READ_EXTERNAL_STORAGE = 12;
|
||||
|
||||
private ArrayList<Uri> mInputUris;
|
||||
private HashMap<Uri, InputDataResult> mInputDataResults;
|
||||
@@ -201,7 +222,9 @@ public class DecryptListFragment
|
||||
);
|
||||
}
|
||||
|
||||
private void displayInputUris(ArrayList<Uri> inputUris, ArrayList<Uri> cancelledUris,
|
||||
private void displayInputUris(
|
||||
ArrayList<Uri> inputUris,
|
||||
ArrayList<Uri> cancelledUris,
|
||||
HashMap<Uri,InputDataResult> results) {
|
||||
|
||||
mInputUris = inputUris;
|
||||
@@ -214,21 +237,19 @@ public class DecryptListFragment
|
||||
for (final Uri uri : inputUris) {
|
||||
mAdapter.add(uri);
|
||||
|
||||
if (mCancelledInputUris.contains(uri)) {
|
||||
mAdapter.setCancelled(uri, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
retryUri(uri);
|
||||
}
|
||||
});
|
||||
boolean uriIsCancelled = mCancelledInputUris.contains(uri);
|
||||
if (uriIsCancelled) {
|
||||
mAdapter.setCancelled(uri, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (results != null && results.containsKey(uri)) {
|
||||
boolean uriHasResult = results != null && results.containsKey(uri);
|
||||
if (uriHasResult) {
|
||||
processResult(uri);
|
||||
} else {
|
||||
mPendingInputUris.add(uri);
|
||||
continue;
|
||||
}
|
||||
|
||||
mPendingInputUris.add(uri);
|
||||
}
|
||||
|
||||
// check if there are any pending input uris
|
||||
@@ -362,12 +383,7 @@ public class DecryptListFragment
|
||||
mCurrentInputUri = null;
|
||||
|
||||
mCancelledInputUris.add(uri);
|
||||
mAdapter.setCancelled(uri, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
retryUri(uri);
|
||||
}
|
||||
});
|
||||
mAdapter.setCancelled(uri, true);
|
||||
|
||||
cryptoOperation();
|
||||
|
||||
@@ -457,8 +473,9 @@ public class DecryptListFragment
|
||||
|
||||
// un-cancel this one
|
||||
mCancelledInputUris.remove(uri);
|
||||
mInputDataResults.remove(uri);
|
||||
mPendingInputUris.add(uri);
|
||||
mAdapter.setCancelled(uri, null);
|
||||
mAdapter.resetItemData(uri);
|
||||
|
||||
// check if there are any pending input uris
|
||||
cryptoOperation();
|
||||
@@ -582,6 +599,11 @@ public class DecryptListFragment
|
||||
@Override
|
||||
public InputDataParcel createOperationInput() {
|
||||
|
||||
Activity activity = getActivity();
|
||||
if (activity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mCurrentInputUri == null) {
|
||||
if (mPendingInputUris.isEmpty()) {
|
||||
// nothing left to do
|
||||
@@ -593,95 +615,102 @@ public class DecryptListFragment
|
||||
|
||||
Log.d(Constants.TAG, "mCurrentInputUri=" + mCurrentInputUri);
|
||||
|
||||
if (readPermissionGranted(mCurrentInputUri)) {
|
||||
PgpDecryptVerifyInputParcel decryptInput = new PgpDecryptVerifyInputParcel()
|
||||
.setAllowSymmetricDecryption(true);
|
||||
return new InputDataParcel(mCurrentInputUri, decryptInput);
|
||||
} else {
|
||||
if ( ! checkAndRequestReadPermission(activity, mCurrentInputUri)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PgpDecryptVerifyInputParcel decryptInput = new PgpDecryptVerifyInputParcel()
|
||||
.setAllowSymmetricDecryption(true);
|
||||
return new InputDataParcel(mCurrentInputUri, decryptInput);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Request READ_EXTERNAL_STORAGE permission on Android >= 6.0 to read content from "file" Uris
|
||||
* Request READ_EXTERNAL_STORAGE permission on Android >= 6.0 to read content from "file" Uris.
|
||||
*
|
||||
* see
|
||||
* https://commonsware.com/blog/2015/10/07/runtime-permissions-files-action-send.html
|
||||
* This method returns true on Android < 6, or if permission is already granted. It
|
||||
* requests the permission and returns false otherwise, taking over responsibility
|
||||
* for mCurrentInputUri.
|
||||
*
|
||||
* see https://commonsware.com/blog/2015/10/07/runtime-permissions-files-action-send.html
|
||||
*/
|
||||
private boolean readPermissionGranted(final Uri uri) {
|
||||
private boolean checkAndRequestReadPermission(Activity activity, final Uri uri) {
|
||||
if ( ! "file".equals(uri.getScheme())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < VERSION_CODES.M) {
|
||||
return true;
|
||||
}
|
||||
if (! "file".equals(uri.getScheme())) {
|
||||
|
||||
// Additional check due to https://commonsware.com/blog/2015/11/09/you-cannot-hold-nonexistent-permissions.html
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Build check due to https://commonsware.com/blog/2015/11/09/you-cannot-hold-nonexistent-permissions.html
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN ||
|
||||
ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
== PackageManager.PERMISSION_GRANTED) {
|
||||
return true;
|
||||
} else {
|
||||
requestPermissions(
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||
MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
|
||||
|
||||
mCurrentInputUri = null;
|
||||
mCancelledInputUris.add(uri);
|
||||
mAdapter.setCancelled(uri, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
retryUri(uri);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
requestPermissions(
|
||||
new String[] { Manifest.permission.READ_EXTERNAL_STORAGE },
|
||||
REQUEST_PERMISSION_READ_EXTERNAL_STORAGE);
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
switch (requestCode) {
|
||||
case MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE: {
|
||||
if (grantResults.length > 0
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
public void onRequestPermissionsResult(int requestCode,
|
||||
@NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
|
||||
// permission granted -> retry all cancelled file uris!
|
||||
for (Iterator<Uri> iterator = mCancelledInputUris.iterator(); iterator.hasNext(); ) {
|
||||
Uri uri = iterator.next();
|
||||
|
||||
if ("file".equals(uri.getScheme())) {
|
||||
iterator.remove();
|
||||
mPendingInputUris.add(uri);
|
||||
mAdapter.setCancelled(uri, null);
|
||||
}
|
||||
}
|
||||
|
||||
// check if there are any pending input uris
|
||||
cryptoOperation();
|
||||
} else {
|
||||
|
||||
// permission denied -> cancel all pending file uris
|
||||
mCurrentInputUri = null;
|
||||
for (final Uri uri : mPendingInputUris) {
|
||||
if ("file".equals(uri.getScheme())) {
|
||||
if (! mCancelledInputUris.contains(uri)) {
|
||||
mCancelledInputUris.add(uri);
|
||||
}
|
||||
mAdapter.setCancelled(uri, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
retryUri(uri);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
if (requestCode != REQUEST_PERMISSION_READ_EXTERNAL_STORAGE) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean permissionWasGranted = grantResults.length > 0
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
if (permissionWasGranted) {
|
||||
|
||||
// permission granted -> retry all cancelled file uris
|
||||
Iterator<Uri> it = mCancelledInputUris.iterator();
|
||||
while (it.hasNext()) {
|
||||
Uri uri = it.next();
|
||||
if ( ! "file".equals(uri.getScheme())) {
|
||||
continue;
|
||||
}
|
||||
it.remove();
|
||||
mPendingInputUris.add(uri);
|
||||
mAdapter.setCancelled(uri, false);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// permission denied -> cancel current, and all pending file uris
|
||||
mCancelledInputUris.add(mCurrentInputUri);
|
||||
mAdapter.setCancelled(mCurrentInputUri, true);
|
||||
|
||||
mCurrentInputUri = null;
|
||||
Iterator<Uri> it = mPendingInputUris.iterator();
|
||||
while (it.hasNext()) {
|
||||
Uri uri = it.next();
|
||||
if ( ! "file".equals(uri.getScheme())) {
|
||||
continue;
|
||||
}
|
||||
it.remove();
|
||||
mCancelledInputUris.add(uri);
|
||||
mAdapter.setCancelled(uri, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// hand control flow back
|
||||
cryptoOperation();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -714,38 +743,83 @@ public class DecryptListFragment
|
||||
return false;
|
||||
}
|
||||
|
||||
private void lookupUnknownKey(final Uri inputUri, long unknownKeyId) {
|
||||
|
||||
final ArrayList<ParcelableKeyRing> keyList;
|
||||
final String keyserver;
|
||||
|
||||
// search config
|
||||
{
|
||||
Preferences prefs = Preferences.getPreferences(getActivity());
|
||||
Preferences.CloudSearchPrefs cloudPrefs =
|
||||
new Preferences.CloudSearchPrefs(true, true, prefs.getPreferredKeyserver());
|
||||
keyserver = cloudPrefs.keyserver;
|
||||
}
|
||||
|
||||
{
|
||||
ParcelableKeyRing keyEntry = new ParcelableKeyRing(null,
|
||||
KeyFormattingUtils.convertKeyIdToHex(unknownKeyId), null);
|
||||
ArrayList<ParcelableKeyRing> selectedEntries = new ArrayList<>();
|
||||
selectedEntries.add(keyEntry);
|
||||
|
||||
keyList = selectedEntries;
|
||||
}
|
||||
|
||||
CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> callback
|
||||
= new CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult>() {
|
||||
|
||||
@Override
|
||||
public ImportKeyringParcel createOperationInput() {
|
||||
return new ImportKeyringParcel(keyList, keyserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCryptoOperationSuccess(ImportKeyResult result) {
|
||||
retryUri(inputUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCryptoOperationCancelled() {
|
||||
mAdapter.setProcessingKeyLookup(inputUri, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCryptoOperationError(ImportKeyResult result) {
|
||||
result.createNotify(getActivity()).show();
|
||||
mAdapter.setProcessingKeyLookup(inputUri, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCryptoSetProgress(String msg, int progress, int max) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
mAdapter.setProcessingKeyLookup(inputUri, true);
|
||||
|
||||
CryptoOperationHelper importOpHelper = new CryptoOperationHelper<>(2, this, callback, null);
|
||||
importOpHelper.cryptoOperation();
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void deleteFile(Activity activity, Uri uri) {
|
||||
|
||||
// we can only ever delete a file once, if we got this far either it's gone or it will never work
|
||||
mCanDelete = false;
|
||||
|
||||
if ("file".equals(uri.getScheme())) {
|
||||
File file = new File(uri.getPath());
|
||||
if (file.delete()) {
|
||||
try {
|
||||
int deleted = FileHelper.deleteFileSecurely(activity, uri);
|
||||
if (deleted > 0) {
|
||||
Notify.create(activity, R.string.file_delete_ok, Style.OK).show();
|
||||
} else {
|
||||
Notify.create(activity, R.string.file_delete_none, Style.WARN).show();
|
||||
}
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
Log.e(Constants.TAG, "exception deleting file", e);
|
||||
Notify.create(activity, R.string.file_delete_exception, Style.ERROR).show();
|
||||
}
|
||||
|
||||
if ("content".equals(uri.getScheme())) {
|
||||
try {
|
||||
int deleted = activity.getContentResolver().delete(uri, null, null);
|
||||
if (deleted > 0) {
|
||||
Notify.create(activity, R.string.file_delete_ok, Style.OK).show();
|
||||
} else {
|
||||
Notify.create(activity, R.string.file_delete_none, Style.WARN).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(Constants.TAG, "exception deleting file", e);
|
||||
Notify.create(activity, R.string.file_delete_exception, Style.ERROR).show();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Notify.create(activity, R.string.file_delete_exception, Style.ERROR).show();
|
||||
|
||||
}
|
||||
|
||||
public class DecryptFilesAdapter extends RecyclerView.Adapter<ViewHolder> {
|
||||
@@ -759,6 +833,7 @@ public class DecryptListFragment
|
||||
int mProgress, mMax;
|
||||
String mProgressMsg;
|
||||
OnClickListener mCancelled;
|
||||
boolean mProcessingKeyLookup;
|
||||
|
||||
ViewModel(Uri uri) {
|
||||
mInputUri = uri;
|
||||
@@ -767,7 +842,7 @@ public class DecryptListFragment
|
||||
mCancelled = null;
|
||||
}
|
||||
|
||||
void addResult(InputDataResult result) {
|
||||
void setResult(InputDataResult result) {
|
||||
mResult = result;
|
||||
}
|
||||
|
||||
@@ -787,6 +862,10 @@ public class DecryptListFragment
|
||||
mMax = max;
|
||||
}
|
||||
|
||||
void setProcessingKeyLookup(boolean processingKeyLookup) {
|
||||
mProcessingKeyLookup = processingKeyLookup;
|
||||
}
|
||||
|
||||
// Depends on inputUri only
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
@@ -797,8 +876,10 @@ public class DecryptListFragment
|
||||
return false;
|
||||
}
|
||||
ViewModel viewModel = (ViewModel) o;
|
||||
return !(mInputUri != null ? !mInputUri.equals(viewModel.mInputUri)
|
||||
: viewModel.mInputUri != null);
|
||||
if (mInputUri == null) {
|
||||
return viewModel.mInputUri == null;
|
||||
}
|
||||
return mInputUri.equals(viewModel.mInputUri);
|
||||
}
|
||||
|
||||
// Depends on inputUri only
|
||||
@@ -853,17 +934,13 @@ public class DecryptListFragment
|
||||
}
|
||||
|
||||
private void bindItemCancelled(ViewHolder holder, ViewModel model) {
|
||||
if (holder.vAnimator.getDisplayedChild() != 3) {
|
||||
holder.vAnimator.setDisplayedChild(3);
|
||||
}
|
||||
holder.vAnimator.setDisplayedChild(3);
|
||||
|
||||
holder.vCancelledRetry.setOnClickListener(model.mCancelled);
|
||||
}
|
||||
|
||||
private void bindItemProgress(ViewHolder holder, ViewModel model) {
|
||||
if (holder.vAnimator.getDisplayedChild() != 0) {
|
||||
holder.vAnimator.setDisplayedChild(0);
|
||||
}
|
||||
holder.vAnimator.setDisplayedChild(0);
|
||||
|
||||
holder.vProgress.setProgress(model.mProgress);
|
||||
holder.vProgress.setMax(model.mMax);
|
||||
@@ -873,11 +950,10 @@ public class DecryptListFragment
|
||||
}
|
||||
|
||||
private void bindItemSuccess(ViewHolder holder, final ViewModel model) {
|
||||
if (holder.vAnimator.getDisplayedChild() != 1) {
|
||||
holder.vAnimator.setDisplayedChild(1);
|
||||
}
|
||||
holder.vAnimator.setDisplayedChild(1);
|
||||
|
||||
KeyFormattingUtils.setStatus(getResources(), holder, model.mResult.mDecryptVerifyResult);
|
||||
KeyFormattingUtils.setStatus(getResources(), holder,
|
||||
model.mResult.mDecryptVerifyResult, model.mProcessingKeyLookup);
|
||||
|
||||
int numFiles = model.mResult.getOutputUris().size();
|
||||
holder.resizeFileList(numFiles, LayoutInflater.from(getActivity()));
|
||||
@@ -955,6 +1031,13 @@ public class DecryptListFragment
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
holder.vSignatureLayout.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
lookupUnknownKey(model.mInputUri, keyId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -983,9 +1066,7 @@ public class DecryptListFragment
|
||||
}
|
||||
|
||||
private void bindItemFailure(ViewHolder holder, final ViewModel model) {
|
||||
if (holder.vAnimator.getDisplayedChild() != 2) {
|
||||
holder.vAnimator.setDisplayedChild(2);
|
||||
}
|
||||
holder.vAnimator.setDisplayedChild(2);
|
||||
|
||||
holder.vErrorMsg.setText(model.mResult.getLog().getLast().mType.getMsgId());
|
||||
|
||||
@@ -1034,21 +1115,44 @@ public class DecryptListFragment
|
||||
notifyItemChanged(pos);
|
||||
}
|
||||
|
||||
public void setCancelled(Uri uri, OnClickListener retryListener) {
|
||||
public void setCancelled(final Uri uri, boolean isCancelled) {
|
||||
ViewModel newModel = new ViewModel(uri);
|
||||
int pos = mDataset.indexOf(newModel);
|
||||
mDataset.get(pos).setCancelled(retryListener);
|
||||
if (isCancelled) {
|
||||
mDataset.get(pos).setCancelled(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
retryUri(uri);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mDataset.get(pos).setCancelled(null);
|
||||
}
|
||||
notifyItemChanged(pos);
|
||||
}
|
||||
|
||||
public void setProcessingKeyLookup(Uri uri, boolean processingKeyLookup) {
|
||||
ViewModel newModel = new ViewModel(uri);
|
||||
int pos = mDataset.indexOf(newModel);
|
||||
mDataset.get(pos).setProcessingKeyLookup(processingKeyLookup);
|
||||
notifyItemChanged(pos);
|
||||
}
|
||||
|
||||
public void addResult(Uri uri, InputDataResult result) {
|
||||
|
||||
ViewModel model = new ViewModel(uri);
|
||||
int pos = mDataset.indexOf(model);
|
||||
model = mDataset.get(pos);
|
||||
model.setResult(result);
|
||||
notifyItemChanged(pos);
|
||||
}
|
||||
|
||||
model.addResult(result);
|
||||
|
||||
public void resetItemData(Uri uri) {
|
||||
ViewModel model = new ViewModel(uri);
|
||||
int pos = mDataset.indexOf(model);
|
||||
model = mDataset.get(pos);
|
||||
model.setResult(null);
|
||||
model.setCancelled(null);
|
||||
model.setProcessingKeyLookup(false);
|
||||
notifyItemChanged(pos);
|
||||
}
|
||||
|
||||
@@ -1072,7 +1176,7 @@ public class DecryptListFragment
|
||||
public View vSignatureLayout;
|
||||
public TextView vSignatureName;
|
||||
public TextView vSignatureMail;
|
||||
public TextView vSignatureAction;
|
||||
public ViewAnimator vSignatureAction;
|
||||
public View vContextMenu;
|
||||
|
||||
public TextView vErrorMsg;
|
||||
@@ -1115,7 +1219,7 @@ public class DecryptListFragment
|
||||
vSignatureLayout = itemView.findViewById(R.id.result_signature_layout);
|
||||
vSignatureName = (TextView) itemView.findViewById(R.id.result_signature_name);
|
||||
vSignatureMail= (TextView) itemView.findViewById(R.id.result_signature_email);
|
||||
vSignatureAction = (TextView) itemView.findViewById(R.id.result_signature_action);
|
||||
vSignatureAction = (ViewAnimator) itemView.findViewById(R.id.result_signature_action);
|
||||
|
||||
vFileList = (LinearLayout) itemView.findViewById(R.id.file_list);
|
||||
for (int i = 0; i < vFileList.getChildCount(); i++) {
|
||||
@@ -1179,7 +1283,7 @@ public class DecryptListFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextView getSignatureAction() {
|
||||
public ViewAnimator getSignatureAction() {
|
||||
return vSignatureAction;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
|
||||
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.ContentDescriptionHint;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify;
|
||||
import org.sufficientlysecure.keychain.util.FabContainer;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
@@ -298,7 +299,7 @@ public class KeyListFragment extends LoaderFragment
|
||||
}
|
||||
|
||||
static final String ORDER =
|
||||
KeyRings.HAS_ANY_SECRET + " DESC, UPPER(" + KeyRings.USER_ID + ") ASC";
|
||||
KeyRings.HAS_ANY_SECRET + " DESC, " + KeyRings.USER_ID + " COLLATE NOCASE ASC";
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
@@ -787,6 +788,8 @@ public class KeyListFragment extends LoaderFragment
|
||||
final KeyItemViewHolder holder = (KeyItemViewHolder) view.getTag();
|
||||
|
||||
holder.mSlinger.setVisibility(View.VISIBLE);
|
||||
|
||||
ContentDescriptionHint.setup(holder.mSlingerButton);
|
||||
holder.mSlingerButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
||||
@@ -82,6 +82,7 @@ import org.sufficientlysecure.keychain.ui.linked.LinkedIdWizard;
|
||||
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State;
|
||||
import org.sufficientlysecure.keychain.ui.util.ContentDescriptionHint;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify.ActionListener;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
|
||||
@@ -182,6 +183,15 @@ public class ViewKeyActivity extends BaseNfcActivity implements
|
||||
mQrCodeLayout = (CardView) findViewById(R.id.view_key_qr_code_layout);
|
||||
|
||||
mRotateSpin = AnimationUtils.loadAnimation(this, R.anim.rotate_spin);
|
||||
|
||||
//ContentDescriptionHint Listeners implemented
|
||||
|
||||
ContentDescriptionHint.setup(mActionEncryptFile);
|
||||
ContentDescriptionHint.setup(mActionEncryptText);
|
||||
ContentDescriptionHint.setup(mActionNfc);
|
||||
ContentDescriptionHint.setup(mFab);
|
||||
|
||||
|
||||
mRotateSpin.setAnimationListener(new AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package org.sufficientlysecure.keychain.ui.util;
|
||||
|
||||
/**
|
||||
* Created by rohan on 20/9/15.
|
||||
*/
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
public class ContentDescriptionHint {
|
||||
private static final int ESTIMATED_TOAST_HEIGHT_DIPS = 48;
|
||||
public static void setup(View view) {
|
||||
view.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
return showLongClickText(view, view.getContentDescription());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void setup(View view, final int textResId) {
|
||||
view.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
return showLongClickText(view, view.getContext().getString(textResId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void setup(View view, final CharSequence text) {
|
||||
view.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
return showLongClickText(view, text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void remove(final View view) {
|
||||
view.setOnLongClickListener(null);
|
||||
}
|
||||
|
||||
private static boolean showLongClickText(View view, CharSequence text) {
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int[] screenPos = new int[2]; // origin is device display
|
||||
final Rect displayFrame = new Rect(); // includes decorations (e.g. status bar)
|
||||
view.getLocationOnScreen(screenPos);
|
||||
view.getWindowVisibleDisplayFrame(displayFrame);
|
||||
|
||||
final Context context = view.getContext();
|
||||
final int viewWidth = view.getWidth();
|
||||
final int viewHeight = view.getHeight();
|
||||
final int viewCenterX = screenPos[0] + viewWidth / 2;
|
||||
final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
|
||||
final int estimatedToastHeight = (int) (ESTIMATED_TOAST_HEIGHT_DIPS
|
||||
* context.getResources().getDisplayMetrics().density);
|
||||
|
||||
Toast longClickText = Toast.makeText(context, text, Toast.LENGTH_SHORT);
|
||||
boolean showBelow = screenPos[1] < estimatedToastHeight;
|
||||
if (showBelow) {
|
||||
// Show below
|
||||
// Offsets are after decorations (e.g. status bar) are factored in
|
||||
longClickText.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL,
|
||||
viewCenterX - screenWidth / 2,
|
||||
screenPos[1] - displayFrame.top + viewHeight);
|
||||
} else {
|
||||
// Show above
|
||||
// Offsets are after decorations (e.g. status bar) are factored in
|
||||
// NOTE: We can't use Gravity.BOTTOM because when the keyboard is up
|
||||
// its height isn't factored in.
|
||||
longClickText.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL,
|
||||
viewCenterX - screenWidth / 2,
|
||||
screenPos[1] - displayFrame.top - estimatedToastHeight);
|
||||
}
|
||||
|
||||
longClickText.show();
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import android.text.style.ForegroundColorSpan;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.ViewAnimator;
|
||||
|
||||
import org.openintents.openpgp.OpenPgpDecryptionResult;
|
||||
import org.openintents.openpgp.OpenPgpSignatureResult;
|
||||
@@ -440,14 +441,15 @@ public class KeyFormattingUtils {
|
||||
View getSignatureLayout();
|
||||
TextView getSignatureUserName();
|
||||
TextView getSignatureUserEmail();
|
||||
TextView getSignatureAction();
|
||||
ViewAnimator getSignatureAction();
|
||||
|
||||
boolean hasEncrypt();
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // context.getDrawable is api lvl 21, need to use deprecated
|
||||
public static void setStatus(Resources resources, StatusHolder holder, DecryptVerifyResult result) {
|
||||
public static void setStatus(Resources resources, StatusHolder holder, DecryptVerifyResult result,
|
||||
boolean processingkeyLookup) {
|
||||
|
||||
if (holder.hasEncrypt()) {
|
||||
OpenPgpDecryptionResult decryptionResult = result.getDecryptionResult();
|
||||
@@ -488,7 +490,7 @@ public class KeyFormattingUtils {
|
||||
OpenPgpSignatureResult signatureResult = result.getSignatureResult();
|
||||
|
||||
int sigText, sigIcon, sigColor;
|
||||
int sigActionText, sigActionIcon;
|
||||
int sigActionDisplayedChild;
|
||||
|
||||
switch (signatureResult.getResult()) {
|
||||
|
||||
@@ -500,8 +502,7 @@ public class KeyFormattingUtils {
|
||||
sigColor = R.color.key_flag_gray;
|
||||
|
||||
// won't be used, but makes compiler happy
|
||||
sigActionText = 0;
|
||||
sigActionIcon = 0;
|
||||
sigActionDisplayedChild = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -510,8 +511,7 @@ public class KeyFormattingUtils {
|
||||
sigIcon = R.drawable.status_signature_verified_cutout_24dp;
|
||||
sigColor = R.color.key_flag_green;
|
||||
|
||||
sigActionText = R.string.decrypt_result_action_show;
|
||||
sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
|
||||
sigActionDisplayedChild = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -520,8 +520,7 @@ public class KeyFormattingUtils {
|
||||
sigIcon = R.drawable.status_signature_unverified_cutout_24dp;
|
||||
sigColor = R.color.key_flag_orange;
|
||||
|
||||
sigActionText = R.string.decrypt_result_action_show;
|
||||
sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
|
||||
sigActionDisplayedChild = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -530,8 +529,7 @@ public class KeyFormattingUtils {
|
||||
sigIcon = R.drawable.status_signature_revoked_cutout_24dp;
|
||||
sigColor = R.color.key_flag_red;
|
||||
|
||||
sigActionText = R.string.decrypt_result_action_show;
|
||||
sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
|
||||
sigActionDisplayedChild = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -540,8 +538,7 @@ public class KeyFormattingUtils {
|
||||
sigIcon = R.drawable.status_signature_expired_cutout_24dp;
|
||||
sigColor = R.color.key_flag_red;
|
||||
|
||||
sigActionText = R.string.decrypt_result_action_show;
|
||||
sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
|
||||
sigActionDisplayedChild = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -550,8 +547,7 @@ public class KeyFormattingUtils {
|
||||
sigIcon = R.drawable.status_signature_unknown_cutout_24dp;
|
||||
sigColor = R.color.key_flag_red;
|
||||
|
||||
sigActionText = R.string.decrypt_result_action_Lookup;
|
||||
sigActionIcon = R.drawable.ic_file_download_grey_24dp;
|
||||
sigActionDisplayedChild = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -560,8 +556,7 @@ public class KeyFormattingUtils {
|
||||
sigIcon = R.drawable.status_signature_invalid_cutout_24dp;
|
||||
sigColor = R.color.key_flag_red;
|
||||
|
||||
sigActionText = R.string.decrypt_result_action_show;
|
||||
sigActionIcon = R.drawable.ic_vpn_key_grey_24dp;
|
||||
sigActionDisplayedChild = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -572,13 +567,17 @@ public class KeyFormattingUtils {
|
||||
sigColor = R.color.key_flag_red;
|
||||
|
||||
// won't be used, but makes compiler happy
|
||||
sigActionText = 0;
|
||||
sigActionIcon = 0;
|
||||
sigActionDisplayedChild = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// possibly switch out "Lookup" button for progress bar
|
||||
if (sigActionDisplayedChild == 1 && processingkeyLookup) {
|
||||
sigActionDisplayedChild = 2;
|
||||
}
|
||||
|
||||
int sigColorRes = resources.getColor(sigColor);
|
||||
holder.getSignatureStatusIcon().setColorFilter(sigColorRes, PorterDuff.Mode.SRC_IN);
|
||||
holder.getSignatureStatusIcon().setImageDrawable(resources.getDrawable(sigIcon));
|
||||
@@ -591,9 +590,7 @@ public class KeyFormattingUtils {
|
||||
|
||||
holder.getSignatureLayout().setVisibility(View.VISIBLE);
|
||||
|
||||
holder.getSignatureAction().setText(sigActionText);
|
||||
holder.getSignatureAction().setCompoundDrawablesWithIntrinsicBounds(
|
||||
0, 0, sigActionIcon, 0);
|
||||
holder.getSignatureAction().setDisplayedChild(sigActionDisplayedChild);
|
||||
|
||||
String userId = result.getSignatureResult().getPrimaryUserId();
|
||||
KeyRing.UserId userIdSplit = KeyRing.splitUserId(userId);
|
||||
|
||||
@@ -25,7 +25,9 @@ import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
@@ -33,7 +35,6 @@ import android.content.ActivityNotFoundException;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Point;
|
||||
@@ -41,20 +42,13 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.StructStat;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
|
||||
import static android.system.OsConstants.S_IROTH;
|
||||
|
||||
/** This class offers a number of helper functions for saving documents.
|
||||
*
|
||||
* There are three entry points here: openDocument, saveDocument and
|
||||
@@ -167,6 +161,14 @@ public class FileHelper {
|
||||
}
|
||||
|
||||
public static long getFileSize(Context context, Uri uri, long def) {
|
||||
if ("file".equals(uri.getScheme())) {
|
||||
long size = new File(uri.getPath()).length();
|
||||
if (size == 0) {
|
||||
size = def;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
long size = def;
|
||||
try {
|
||||
Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null);
|
||||
@@ -261,6 +263,44 @@ public class FileHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes data at a URI securely by overwriting it with random data
|
||||
* before deleting it. This method is fail-fast - if we can't securely
|
||||
* delete the file, we don't delete it at all.
|
||||
*/
|
||||
public static int deleteFileSecurely(Context context, Uri uri)
|
||||
throws IOException {
|
||||
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
long lengthLeft = FileHelper.getFileSize(context, uri);
|
||||
|
||||
if (lengthLeft == -1) {
|
||||
throw new IOException("Error opening file!");
|
||||
}
|
||||
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] randomData = new byte[1024];
|
||||
|
||||
OutputStream out = resolver.openOutputStream(uri, "w");
|
||||
if (out == null) {
|
||||
throw new IOException("Error opening file!");
|
||||
}
|
||||
out = new BufferedOutputStream(out);
|
||||
while (lengthLeft > 0) {
|
||||
random.nextBytes(randomData);
|
||||
out.write(randomData, 0, lengthLeft > randomData.length ? randomData.length : (int) lengthLeft);
|
||||
lengthLeft -= randomData.length;
|
||||
}
|
||||
out.close();
|
||||
|
||||
if ("file".equals(uri.getScheme())) {
|
||||
return new File(uri.getPath()).delete() ? 1 : 0;
|
||||
} else {
|
||||
return resolver.delete(uri, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Checks if external storage is mounted if file is located on external storage. */
|
||||
public static boolean isStorageMounted(String file) {
|
||||
if (file.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) {
|
||||
|
||||
Reference in New Issue
Block a user