diff --git a/app/build.gradle b/app/build.gradle index a877e8d..f28f758 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "com.github.axet.audiorecorder" minSdkVersion 9 targetSdkVersion 23 - versionCode 164 - versionName "2.1.0" + versionCode 165 + versionName "3.0.0" } signingConfigs { release { @@ -43,5 +43,5 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' - compile 'com.github.axet:android-audio-library:0.1.0' // compile project(':android-audio-library') + compile 'com.github.axet:android-audio-library:1.0.0' // compile project(':android-audio-library') } diff --git a/app/src/main/java/com/github/axet/audiorecorder/activities/MainActivity.java b/app/src/main/java/com/github/axet/audiorecorder/activities/MainActivity.java index 1319382..570873a 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/activities/MainActivity.java +++ b/app/src/main/java/com/github/axet/audiorecorder/activities/MainActivity.java @@ -3,6 +3,7 @@ package com.github.axet.audiorecorder.activities; import android.Manifest; import android.app.AlertDialog; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -10,12 +11,18 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Configuration; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.ParcelFileDescriptor; import android.preference.PreferenceManager; +import android.provider.DocumentsContract; import android.support.design.widget.FloatingActionButton; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; +import android.system.ErrnoException; +import android.system.Os; +import android.system.StructStatVfs; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -33,6 +40,7 @@ import com.github.axet.audiorecorder.app.MainApplication; import com.github.axet.audiorecorder.services.RecordingService; import java.io.File; +import java.io.FileNotFoundException; import java.util.Collections; public class MainActivity extends AppCompatActivity { @@ -130,7 +138,7 @@ public class MainActivity extends AppCompatActivity { } Intent showIntent() { - Uri selectedUri = Uri.fromFile(storage.getStoragePath()); + Uri selectedUri = storage.getStoragePath(); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(selectedUri, "resource/folder"); return intent; @@ -187,12 +195,10 @@ public class MainActivity extends AppCompatActivity { return; } - if (Storage.permitted(MainActivity.this, PERMISSIONS)) { - try { - storage.migrateLocalStorage(); - } catch (RuntimeException e) { - Error(e); - } + try { + storage.migrateLocalStorage(); + } catch (RuntimeException e) { + Error(e); } final int selected = getLastRecording(); @@ -225,11 +231,9 @@ public class MainActivity extends AppCompatActivity { int getLastRecording() { final SharedPreferences shared = PreferenceManager.getDefaultSharedPreferences(this); String last = shared.getString(MainApplication.PREFERENCE_LAST, ""); - last = last.toLowerCase(); for (int i = 0; i < recordings.getCount(); i++) { - File f = recordings.getItem(i); - String n = f.getName().toLowerCase(); - if (n.equals(last)) { + Uri f = recordings.getItem(i); + if (storage.getDocumentName(f).equals(last)) { SharedPreferences.Editor edit = shared.edit(); edit.putString(MainApplication.PREFERENCE_LAST, ""); edit.commit(); @@ -277,8 +281,8 @@ public class MainActivity extends AppCompatActivity { } void updateHeader() { - File f = storage.getStoragePath(); - long free = Storage.getFree(f); + Uri uri = storage.getStoragePath(); + long free = storage.getFree(uri); long sec = Storage.average(this, free); TextView text = (TextView) findViewById(R.id.space_left); text.setText(MainApplication.formatFree(this, free, sec)); diff --git a/app/src/main/java/com/github/axet/audiorecorder/activities/RecordingActivity.java b/app/src/main/java/com/github/axet/audiorecorder/activities/RecordingActivity.java index cfbf145..c4133b2 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/activities/RecordingActivity.java +++ b/app/src/main/java/com/github/axet/audiorecorder/activities/RecordingActivity.java @@ -4,6 +4,7 @@ import android.Manifest; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -13,10 +14,12 @@ import android.content.pm.PackageManager; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaRecorder; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; +import android.provider.DocumentsContract; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.ActionBar; @@ -36,6 +39,7 @@ import android.widget.Toast; import com.github.axet.androidlibrary.animations.MarginBottomAnimation; import com.github.axet.androidlibrary.sound.AudioTrack; import com.github.axet.audiolibrary.app.RawSamples; +import com.github.axet.audiolibrary.app.Recordings; import com.github.axet.audiolibrary.app.Sound; import com.github.axet.audiolibrary.encoders.Encoder; import com.github.axet.audiolibrary.encoders.EncoderInfo; @@ -47,7 +51,13 @@ import com.github.axet.audiorecorder.app.MainApplication; import com.github.axet.audiorecorder.app.Storage; import com.github.axet.audiorecorder.services.RecordingService; +import org.apache.commons.io.IOUtils; + import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.ShortBuffer; @@ -75,7 +85,7 @@ public class RecordingActivity extends AppCompatActivity { // pitch size in samples. how many samples count need to update view. 4410 for 100ms update. int samplesUpdate; // output target file 2016-01-01 01.01.01.wav - File targetFile = null; + Uri targetUri = null; // how many samples passed for current recording long samplesTime; // current cut position in samples from begining of file @@ -172,22 +182,26 @@ public class RecordingActivity extends AppCompatActivity { try { if (storage.recordingPending()) { String file = shared.getString(MainApplication.PREFERENCE_TARGET, null); - if (file != null) - targetFile = new File(file); + if (file != null) { + if (file.startsWith(ContentResolver.SCHEME_CONTENT)) + targetUri = Uri.parse(file); + else + targetUri = Uri.fromFile(new File(file)); + } } - if (targetFile == null) - targetFile = storage.getNewFile(); + if (targetUri == null) + targetUri = storage.getNewFile(); SharedPreferences.Editor editor = shared.edit(); - editor.putString(MainApplication.PREFERENCE_TARGET, targetFile.toString()); + editor.putString(MainApplication.PREFERENCE_TARGET, targetUri.toString()); editor.commit(); } catch (RuntimeException e) { + Log.d(TAG, "onCreate", e); Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); finish(); return; } - title.setText(targetFile.getName()); - + title.setText(storage.getDocumentName(targetUri)); if (shared.getBoolean(MainApplication.PREFERENCE_CALL, false)) { TelephonyManager tm = (TelephonyManager) this.getSystemService(Context.TELEPHONY_SERVICE); @@ -343,7 +357,7 @@ public class RecordingActivity extends AppCompatActivity { boolean recording = thread != null; - RecordingService.startService(this, targetFile.getName(), recording, encoder != null); + RecordingService.startService(this, storage.getDocumentName(targetUri), recording, encoder != null); if (recording) { pitch.record(); @@ -368,7 +382,7 @@ public class RecordingActivity extends AppCompatActivity { stopRecording(); - RecordingService.startService(this, targetFile.getName(), thread != null, encoder != null); + RecordingService.startService(this, storage.getDocumentName(targetUri), thread != null, encoder != null); pitch.setOnTouchListener(new View.OnTouchListener() { @Override @@ -740,7 +754,7 @@ public class RecordingActivity extends AppCompatActivity { }, "RecordingThread"); thread.start(); - RecordingService.startService(this, targetFile.getName(), thread != null, encoder != null); + RecordingService.startService(this, storage.getDocumentName(targetUri), thread != null, encoder != null); } // calcuale buffer length dynamically, this way we can reduce thread cycles when activity in background @@ -823,9 +837,9 @@ public class RecordingActivity extends AppCompatActivity { void encoding(final Runnable run) { final File in = storage.getTempRecording(); - final File out = targetFile; + final File out = storage.getTempEncoding(); - File parent = targetFile.getParentFile(); + File parent = out.getParentFile(); if (!parent.exists()) { if (!parent.mkdirs()) { // in case if it were manually deleted @@ -844,11 +858,11 @@ public class RecordingActivity extends AppCompatActivity { encoder = new FileEncoder(this, in, e); - RecordingService.startService(this, targetFile.getName(), thread != null, encoder != null); + RecordingService.startService(this, storage.getDocumentName(targetUri), thread != null, encoder != null); final ProgressDialog d = new ProgressDialog(this); d.setTitle(R.string.encoding_title); - d.setMessage(".../" + targetFile.getName()); + d.setMessage(".../" + storage.getDocumentName(targetUri)); d.setMax(100); d.setCancelable(false); d.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); @@ -864,10 +878,32 @@ public class RecordingActivity extends AppCompatActivity { @Override public void run() { d.cancel(); - storage.delete(in); + + ContentResolver resolver = getContentResolver(); + try { + InputStream is = new FileInputStream(out); + OutputStream os = resolver.openOutputStream(targetUri); + IOUtils.copy(is, os); + is.close(); + os.close(); + } catch (IOException e) { + storage.delete(out); // delete tmp encoding file + try { + storage.delete(targetUri); + } catch (RuntimeException ee) { + Log.d(TAG, "unable to delete target uri", e); // ignore, not even created? + } + Error(e); + d.cancel(); + return; + } + + storage.delete(in); // delete raw recording + String n = out.getName(); + storage.delete(out); // delete tmp encoding file SharedPreferences.Editor edit = shared.edit(); - edit.putString(MainApplication.PREFERENCE_LAST, out.getName()); + edit.putString(MainApplication.PREFERENCE_LAST, n); edit.commit(); run.run(); diff --git a/app/src/main/java/com/github/axet/audiorecorder/activities/SettingsActivity.java b/app/src/main/java/com/github/axet/audiorecorder/activities/SettingsActivity.java index a1a4407..d04f709 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/activities/SettingsActivity.java +++ b/app/src/main/java/com/github/axet/audiorecorder/activities/SettingsActivity.java @@ -244,7 +244,8 @@ public class SettingsActivity extends AppCompatActivity implements SharedPrefere bindPreferenceSummaryToValue(pm.findPreference(MainApplication.PREFERENCE_CHANNELS)); bindPreferenceSummaryToValue(pm.findPreference(MainApplication.PREFERENCE_FORMAT)); StoragePathPreferenceCompat s = (StoragePathPreferenceCompat) pm.findPreference(MainApplication.PREFERENCE_STORAGE); - s.setPermissionsDialog(this, PERMISSIONS, 1); +// s.setPermissionsDialog(this, PERMISSIONS, 1); + s.setStorageAccessFramework(this, 2); } @Override @@ -281,6 +282,19 @@ public class SettingsActivity extends AppCompatActivity implements SharedPrefere } } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + StoragePathPreferenceCompat s = (StoragePathPreferenceCompat) findPreference(MainApplication.PREFERENCE_STORAGE); + + switch (requestCode) { + case 2: + s.onActivityResult(resultCode, data); + break; + } + } + @Override public void onResume() { super.onResume(); diff --git a/app/src/main/java/com/github/axet/audiorecorder/app/Storage.java b/app/src/main/java/com/github/axet/audiorecorder/app/Storage.java index f9ece44..6b7f477 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/app/Storage.java +++ b/app/src/main/java/com/github/axet/audiorecorder/app/Storage.java @@ -1,8 +1,13 @@ package com.github.axet.audiorecorder.app; +import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; import android.preference.PreferenceManager; +import android.provider.DocumentsContract; +import android.webkit.MimeTypeMap; import java.io.File; import java.text.SimpleDateFormat; @@ -10,13 +15,13 @@ import java.util.Date; public class Storage extends com.github.axet.audiolibrary.app.Storage { + public static final String TMP_ENC = "encoding.data"; + public Storage(Context context) { super(context); } - @Override - public File getNewFile() { - + public Uri getNewFile() { SharedPreferences shared = PreferenceManager.getDefaultSharedPreferences(context); String ext = shared.getString(MainApplication.PREFERENCE_ENCODING, ""); @@ -28,13 +33,55 @@ public class Storage extends com.github.axet.audiolibrary.app.Storage { format = format.replaceAll("%I", ISO8601.format(new Date())); format = format.replaceAll("%T", "" + System.currentTimeMillis() / 1000); - File parent = getStoragePath(); - if (!parent.exists()) { - if (!parent.mkdirs()) - throw new RuntimeException("Unable to create: " + parent); - } + Uri path = getStoragePath(); + String s = path.getScheme(); - return getNextFile(parent, format, ext); + if (Build.VERSION.SDK_INT >= 21 && s.startsWith(ContentResolver.SCHEME_CONTENT)) { + Uri n = getNextFile(path, format, ext); + String d = getDocumentName(n); + Uri docUri = DocumentsContract.buildDocumentUriUsingTree(path, DocumentsContract.getTreeDocumentId(path)); + String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(d); + Uri childrenUri = DocumentsContract.createDocument(context.getContentResolver(), docUri, mime, d); + return childrenUri; + } else if (s.startsWith(ContentResolver.SCHEME_FILE)) { + File f = new File(path.getPath()); + if (!f.exists() && !f.mkdirs()) { + throw new RuntimeException("Unable to create: " + path); + } + return Uri.fromFile(getNextFile(f, format, ext)); + } else { + throw new RuntimeException("unknown uri"); + } } + + public File getTempEncoding() { + File internal = new File(context.getCacheDir(), TMP_ENC); + if (internal.exists()) + return internal; + + // Starting in KITKAT, no permissions are required to read or write to the returned path; + // it's always accessible to the calling app. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + if (!permitted(context, PERMISSIONS)) + return internal; + } + + File c = context.getExternalCacheDir(); + if (c == null) // some old phones <15API with disabled sdcard return null + return internal; + + File external = new File(c, TMP_ENC); + + if (external.exists()) + return external; + + long freeI = getFree(internal); + long freeE = getFree(external); + + if (freeI > freeE) + return internal; + else + return external; + } } diff --git a/app/src/main/java/com/github/axet/audiorecorder/services/RecordingService.java b/app/src/main/java/com/github/axet/audiorecorder/services/RecordingService.java index 9d46d7e..715696d 100644 --- a/app/src/main/java/com/github/axet/audiorecorder/services/RecordingService.java +++ b/app/src/main/java/com/github/axet/audiorecorder/services/RecordingService.java @@ -9,6 +9,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Build; import android.os.IBinder; import android.preference.PreferenceManager; @@ -171,8 +172,8 @@ public class RecordingService extends Service { String text; if (targetFile == null) { title = getString(R.string.app_name); - File f = storage.getStoragePath(); - long free = Storage.getFree(f); + Uri f = storage.getStoragePath(); + long free = storage.getFree(f); long sec = Storage.average(this, free); text = MainApplication.formatFree(this, free, sec); view.setViewVisibility(R.id.notification_record, View.VISIBLE); @@ -227,7 +228,6 @@ public class RecordingService extends Service { } } - @Override public void onTaskRemoved(Intent rootIntent) { super.onTaskRemoved(rootIntent); diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index 331c2b4..7e567bb 100644 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -54,8 +54,7 @@