Commit a7fa15c7 authored by Joey's avatar Joey
Browse files

Recorder: friendship with storage ended, MediaProvider is my new best friend


Signed-off-by: default avatarJoey <joey@lineageos.org>
Change-Id: I73fb54386f7d26b9662f0c95a201b49ebfdc4944
parent 7f8a0979
......@@ -19,11 +19,14 @@ import android.Manifest;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
......@@ -88,7 +91,7 @@ public class DialogActivity extends AppCompatActivity implements
setupAsSettingsScreen();
}
animateAppareance();
animateAppearance();
boolean deleteLastRecording = intent.getBooleanExtra(EXTRA_DELETE_LAST_RECORDING, false);
if (deleteLastRecording) {
......@@ -126,7 +129,7 @@ public class DialogActivity extends AppCompatActivity implements
}
}
private void animateAppareance() {
private void animateAppearance() {
mRootView.setAlpha(0f);
mRootView.animate()
.alpha(1f)
......@@ -150,21 +153,24 @@ public class DialogActivity extends AppCompatActivity implements
private void playLastItem(boolean isSound) {
String type = isSound ? TYPE_AUDIO : TYPE_VIDEO;
String path = LastRecordHelper.getLastItemPath(this, isSound);
startActivityForResult(LastRecordHelper.getOpenIntent(this, path, type), 0);
Uri uri = LastRecordHelper.getLastItemUri(this, isSound);
Intent intent = LastRecordHelper.getOpenIntent(uri, type);
if (intent != null) {
startActivityForResult(intent, 0);
}
}
private void deleteLastItem(boolean isSound) {
String path = LastRecordHelper.getLastItemPath(this, isSound);
AlertDialog dialog = LastRecordHelper.deleteFile(this, path, isSound);
Uri uri = LastRecordHelper.getLastItemUri(this, isSound);
AlertDialog dialog = LastRecordHelper.deleteFile(this, uri, isSound);
dialog.setOnDismissListener(d -> finish());
dialog.show();
}
private void shareLastItem(boolean isSound) {
String type = isSound ? TYPE_AUDIO : TYPE_VIDEO;
String path = LastRecordHelper.getLastItemPath(this, isSound);
startActivity(LastRecordHelper.getShareIntent(this, path, type));
Uri uri = LastRecordHelper.getLastItemUri(this, isSound);
startActivity(LastRecordHelper.getShareIntent(uri, type));
}
private void setupAsSettingsScreen() {
......
......@@ -253,7 +253,6 @@ public class RecorderActivity extends AppCompatActivity implements
if (mSoundService.isRecording()) {
// Stop
mSoundService.stopRecording();
mSoundService.createShareNotification();
stopService(new Intent(this, SoundRecorderService.class));
Utils.setStatus(this, Utils.UiStatus.NOTHING);
} else {
......@@ -279,6 +278,10 @@ public class RecorderActivity extends AppCompatActivity implements
// Start
MediaProjectionManager mediaProjectionManager = getSystemService(
MediaProjectionManager.class);
if (mediaProjectionManager == null) {
return;
}
Intent permissionIntent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(permissionIntent, REQUEST_AUDIO_VIDEO);
}
......@@ -432,8 +435,8 @@ public class RecorderActivity extends AppCompatActivity implements
}
private void updateLastItemStatus() {
String lastScreen = LastRecordHelper.getLastItemPath(this, false);
String lastSound = LastRecordHelper.getLastItemPath(this, true);
Uri lastScreen = LastRecordHelper.getLastItemUri(this, false);
Uri lastSound = LastRecordHelper.getLastItemUri(this, true);
if (lastScreen == null) {
mScreenLast.setVisibility(View.GONE);
......
......@@ -30,6 +30,7 @@ import android.hardware.display.VirtualDisplay;
import android.media.MediaRecorder;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.net.Uri;
import android.os.Environment;
import android.os.IBinder;
import android.os.StatFs;
......@@ -40,10 +41,12 @@ import android.util.Log;
import android.view.Surface;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import org.lineageos.recorder.R;
import org.lineageos.recorder.RecorderActivity;
import org.lineageos.recorder.utils.MediaProviderHelper;
import org.lineageos.recorder.utils.LastRecordHelper;
import org.lineageos.recorder.utils.Utils;
......@@ -55,7 +58,7 @@ import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
public class ScreencastService extends Service {
public class ScreencastService extends Service implements MediaProviderHelper.OnContentWritten {
private static final String LOGTAG = "ScreencastService";
private static final String SCREENCAST_NOTIFICATION_CHANNEL =
......@@ -190,6 +193,14 @@ public class ScreencastService extends Service {
super.onDestroy();
}
@Override
public void onContentWritten(@Nullable String uri) {
stopForeground(true);
if (uri != null) {
sendShareNotification(uri);
}
}
private int startScreencasting(Intent intent) {
if (hasNoAvailableSpace()) {
Toast.makeText(this, R.string.screen_insufficient_storage,
......@@ -299,10 +310,8 @@ public class ScreencastService extends Service {
mTimer.cancel();
mTimer = null;
}
stopForeground(true);
if (mPath != null) {
sendShareNotification(mPath.getPath());
}
MediaProviderHelper.addVideoToContentProvider(getContentResolver(), mPath, this);
}
private void stopCasting() {
......@@ -334,24 +343,23 @@ public class ScreencastService extends Service {
mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
}
private NotificationCompat.Builder createShareNotificationBuilder(String file) {
private NotificationCompat.Builder createShareNotificationBuilder(String uriStr) {
Uri uri = Uri.parse(uriStr);
Intent intent = new Intent(this, RecorderActivity.class);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
PendingIntent playPIntent = PendingIntent.getActivity(this, 0,
LastRecordHelper.getOpenIntent(this, file, "video/mp4"),
LastRecordHelper.getOpenIntent(uri, "video/mp4"),
PendingIntent.FLAG_CANCEL_CURRENT);
PendingIntent sharePIntent = PendingIntent.getActivity(this, 0,
LastRecordHelper.getShareIntent(this, file, "video/mp4"),
LastRecordHelper.getShareIntent(uri, "video/mp4"),
PendingIntent.FLAG_CANCEL_CURRENT);
PendingIntent deletePIntent = PendingIntent.getActivity(this, 0,
LastRecordHelper.getDeleteIntent(this, false),
PendingIntent.FLAG_CANCEL_CURRENT);
long timeElapsed = SystemClock.elapsedRealtime() - mStartTime;
LastRecordHelper.setLastItem(this, file, timeElapsed, false);
Log.i(LOGTAG, "Video complete: " + file);
LastRecordHelper.setLastItem(this, uriStr, timeElapsed, false);
return new NotificationCompat.Builder(this, SCREENCAST_NOTIFICATION_CHANNEL)
.setWhen(System.currentTimeMillis())
......
......@@ -27,9 +27,12 @@ import android.content.IntentFilter;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import android.text.format.DateUtils;
......@@ -38,6 +41,7 @@ import android.util.Log;
import org.lineageos.recorder.R;
import org.lineageos.recorder.RecorderActivity;
import org.lineageos.recorder.utils.LastRecordHelper;
import org.lineageos.recorder.utils.MediaProviderHelper;
import org.lineageos.recorder.utils.Utils;
import java.io.BufferedOutputStream;
......@@ -50,7 +54,7 @@ import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
public class SoundRecorderService extends Service {
public class SoundRecorderService extends Service implements MediaProviderHelper.OnContentWritten {
static final String EXTENSION = ".pcm";
private static final String ACTION_STARTED = "org.lineageos.recorder.sounds.STARTED_SOUND";
......@@ -134,6 +138,17 @@ public class SoundRecorderService extends Service {
super.onDestroy();
}
@Override
public void onContentWritten(@Nullable String uri) {
mStatus = RecorderStatus.STOPPED;
mOutFilePath = uri;
Intent intent = new Intent(ACTION_STOPPED);
intent.putExtra(EXTRA_FILE, mOutFilePath);
sendBroadcast(intent);
createShareNotification();
stopForeground(true);
}
public boolean isRecording() {
return mStatus == RecorderStatus.RECORDING;
}
......@@ -201,11 +216,8 @@ public class SoundRecorderService extends Service {
oldFile.delete();
}
mStatus = RecorderStatus.STOPPED;
Intent intent = new Intent(ACTION_STOPPED);
intent.putExtra(EXTRA_FILE, mOutFilePath);
sendBroadcast(intent);
stopForeground(true);
MediaProviderHelper.addSoundToContentProvider(
getContentResolver(), new File(mOutFilePath), this);
}
private File createNewAudioFile() {
......@@ -307,14 +319,15 @@ public class SoundRecorderService extends Service {
}
public void createShareNotification() {
Uri outFileUri = Uri.parse(mOutFilePath);
Intent intent = new Intent(this, RecorderActivity.class);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
PendingIntent playPIntent = PendingIntent.getActivity(this, 0,
LastRecordHelper.getOpenIntent(this, mOutFilePath, "audio/wav"),
LastRecordHelper.getOpenIntent(outFileUri, "audio/wav"),
PendingIntent.FLAG_CANCEL_CURRENT);
PendingIntent sharePIntent = PendingIntent.getActivity(this, 0,
LastRecordHelper.getShareIntent(this, mOutFilePath, "audio/wav"),
LastRecordHelper.getShareIntent(outFileUri, "audio/wav"),
PendingIntent.FLAG_CANCEL_CURRENT);
PendingIntent deletePIntent = PendingIntent.getActivity(this, 0,
LastRecordHelper.getDeleteIntent(this, true),
......
......@@ -20,7 +20,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import androidx.core.content.FileProvider;
import androidx.appcompat.app.AlertDialog;
import org.lineageos.recorder.DialogActivity;
......@@ -36,21 +36,16 @@ public class LastRecordHelper {
private static final String KEY_LAST_SCREEN = "screen_last_path";
private static final String KEY_LAST_SOUND_TIME = "sound_last_duration";
private static final String KEY_LAST_SCREEN_TIME = "screen_last_duration";
private static final String FILE_PROVIDER = "org.lineageos.recorder.fileprovider";
private LastRecordHelper() {
}
public static AlertDialog deleteFile(Context context, final String path, boolean isSound) {
public static AlertDialog deleteFile(Context context, final Uri uri, boolean isSound) {
return new AlertDialog.Builder(context)
.setTitle(R.string.delete_title)
.setMessage(context.getString(R.string.delete_message, path))
.setMessage(context.getString(R.string.delete_message, uri))
.setPositiveButton(R.string.delete, (dialog, which) -> {
File record = new File(path);
if (record.exists()) {
//noinspection ResultOfMethodCallIgnored
record.delete();
}
MediaProviderHelper.remove(context.getContentResolver(), uri);
NotificationManager nm = context.getSystemService(NotificationManager.class);
if (nm == null) {
return;
......@@ -67,20 +62,15 @@ public class LastRecordHelper {
.create();
}
public static Intent getShareIntent(Context context, String filePath, String mimeType) {
File file = new File(filePath);
Uri uri = FileProvider.getUriForFile(context, FILE_PROVIDER, file);
public static Intent getShareIntent(Uri uri, String mimeType) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(mimeType);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.putExtra(Intent.EXTRA_SUBJECT, file.getName());
intent.setDataAndType(uri, mimeType);
Intent chooserIntent = Intent.createChooser(intent, null);
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
return chooserIntent;
}
public static Intent getOpenIntent(Context context, String filePath, String mimeType) {
Uri uri = FileProvider.getUriForFile(context, FILE_PROVIDER, new File(filePath));
public static Intent getOpenIntent(Uri uri, String mimeType) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, mimeType);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
......@@ -106,9 +96,10 @@ public class LastRecordHelper {
.apply();
}
public static String getLastItemPath(Context context, boolean isSound) {
public static Uri getLastItemUri(Context context, boolean isSound) {
SharedPreferences prefs = context.getSharedPreferences(PREFS, 0);
return prefs.getString(isSound ? KEY_LAST_SOUND : KEY_LAST_SCREEN, null);
String uriStr = prefs.getString(isSound ? KEY_LAST_SOUND : KEY_LAST_SCREEN, null);
return uriStr == null ? null : Uri.parse(uriStr);
}
private static long getLastItemDuration(Context context, boolean isSound) {
......@@ -116,20 +107,8 @@ public class LastRecordHelper {
return prefs.getLong(isSound ? KEY_LAST_SOUND_TIME : KEY_LAST_SCREEN_TIME, -1);
}
private static String getLastItemDate(Context context, boolean isSound) {
String path = getLastItemPath(context, isSound);
String[] pathParts = path.split("/");
String[] date = pathParts[pathParts.length - 1]
.replace(isSound ? ".wav" : ".mp4", "")
.replace(isSound ? "SoundRecord" : "ScreenRecord", "")
.split("-");
return context.getString(R.string.date_format, date[1], date[2], date[3],
date[4], date[5]);
}
public static String getLastItemDescription(Context context, boolean isSound) {
return context.getString(R.string.screen_last_message,
getLastItemDate(context, isSound),
getLastItemDuration(context, isSound) / 1000);
}
}
/*
* Copyright (C) 2019 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.lineageos.recorder.utils;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
public final class MediaProviderHelper {
private static final String TAG = "MediaProviderHelper";
private MediaProviderHelper() {
}
public static void addSoundToContentProvider(
@Nullable ContentResolver cr,
@Nullable File file,
@NonNull OnContentWritten listener) {
if (Build.VERSION.SDK_INT < 29 || cr == null || file == null) {
return;
}
final ContentValues values = new ContentValues();
values.put(MediaStore.Audio.Media.DISPLAY_NAME, file.getName());
values.put(MediaStore.Audio.Media.TITLE, file.getName());
values.put(MediaStore.Audio.Media.MIME_TYPE, "audio/x-wav");
values.put(MediaStore.Audio.Media.ARTIST, "Recorder");
values.put(MediaStore.Audio.Media.ALBUM, "Sound records");
values.put(MediaStore.Audio.Media.DATE_ADDED, System.currentTimeMillis() / 1000L);
values.put(MediaStore.Audio.Media.RELATIVE_PATH, "Music/Sound records");
values.put(MediaStore.Audio.Media.IS_PENDING, 1);
final Uri uri = cr.insert(MediaStore.Audio.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY), values);
if (uri == null) {
Log.e(TAG, "Failed to insert " + file.getAbsolutePath());
return;
}
new WriterTask(file, uri, cr, listener).execute();
}
public static void addVideoToContentProvider(
@Nullable ContentResolver cr,
@Nullable File file,
@NonNull OnContentWritten listener) {
if (Build.VERSION.SDK_INT < 29 || cr == null || file == null) {
return;
}
final ContentValues values = new ContentValues();
values.put(MediaStore.Video.Media.DISPLAY_NAME, file.getName());
values.put(MediaStore.Video.Media.TITLE, file.getName());
values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000L);
values.put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/Screen records");
values.put(MediaStore.Audio.Media.IS_PENDING, 1);
final Uri uri = cr.insert(MediaStore.Video.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY), values);
if (uri == null) {
Log.e(TAG, "Failed to insert " + file.getAbsolutePath());
return;
}
new WriterTask(file, uri, cr, listener).execute();
}
static void remove(@NonNull ContentResolver cr, @NonNull Uri uri) {
cr.delete(uri, null, null);
}
@RequiresApi(29)
public static class WriterTask extends AsyncTask<Void, Void, String> {
@NonNull
private File file;
@NonNull
private Uri uri;
@NonNull
private ContentResolver cr;
@NonNull
private OnContentWritten listener;
/* synthetic */ WriterTask(@NonNull File file,
@NonNull Uri uri,
@NonNull ContentResolver cr,
@NonNull OnContentWritten listener) {
this.file = file;
this.uri = uri;
this.cr = cr;
this.listener = listener;
}
@Override
protected String doInBackground(Void... voids) {
try {
final ParcelFileDescriptor pfd = cr.openFileDescriptor(uri, "w", null);
if (pfd == null) {
return null;
}
final FileOutputStream oStream = new FileOutputStream(pfd.getFileDescriptor());
oStream.write(Files.readAllBytes(file.toPath()));
oStream.close();
pfd.close();
final ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.IS_PENDING, 0);
cr.update(uri, values, null, null);
if (!file.delete()) {
Log.w(TAG, "Failed to delete tmp file");
}
return uri.toString();
} catch (IOException e) {
Log.e(TAG, "Failed to write into MediaStore", e);
return null;
}
}
@Override
protected void onPostExecute(String s) {
listener.onContentWritten(s);
}
}
public interface OnContentWritten {
void onContentWritten(@Nullable String uri);
}
}
......@@ -57,6 +57,7 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/sound"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHeight_default="percent"
app:layout_constraintHeight_percent="0.5"
app:layout_constraintLeft_toLeftOf="parent"
......
......@@ -37,7 +37,6 @@
<string name="screen_notification_message_done">Gereed om te deel</string>
<string name="screen_recording_message">Skerm word opgeneem\u2026</string>
<string name="screen_last_title">Laaste opname</string>
<string name="screen_last_message">Datum: %1$s\nLengte: %2$d sekondes</string>
<string name="screen_overlay_notif_title">Gereed om op te neem</string>
<string name="screen_overlay_notif_message">Druk die opneem-knoppie wanneer jy gereed is om te begin</string>
<string name="dialog_permissions_title">Toestemmings</string>
......
......@@ -41,7 +41,6 @@
<string name="screen_notification_message_done">جاهز للمشاركة</string>
<string name="screen_recording_message">يتم تسجيل الشاشة</string>
<string name="screen_last_title">التسجيل الأخير</string>
<string name="screen_last_message">التاريخ: %1$s\nالمدة: %2$d ثواني</string>
<string name="screen_overlay_notif_title">مستعد للتسجيل</string>
<string name="screen_overlay_notif_message">اضغط على زر التسجيل عندما تكون على استعداد للبدء</string>
<string name="screen_settings_title">ضبط إعدادات مسجل الشاشة</string>
......
......@@ -41,7 +41,6 @@
<string name="screen_notification_message_done">Preparada pa compartir</string>
<string name="screen_recording_message">Ta grabándose la pantalla\u2026</string>
<string name="screen_last_title">Grabación cabera</string>
<string name="screen_last_message">Data: %1$s\nDuración: %2$d segundos</string>
<string name="screen_overlay_notif_title">Preparada pa grabar</string>
<string name="screen_overlay_notif_message">Primi\'l botón de grabar cuando teas preparáu</string>
<string name="screen_settings_title">Axustes de grabación de la pantalla</string>
......
......@@ -41,7 +41,6 @@
<string name="screen_notification_message_done">Paylaşmaq üçün hazırdır</string>
<string name="screen_recording_message">Ekran yazılmağa başladı\u2026</string>
<string name="screen_last_title">Son yazma</string>
<string name="screen_last_message">Tarix: %1$s\nMüddəti: %2$d saniyə</string>
<string name="screen_overlay_notif_title">Yazmaq üçün hazırdır</string>
<string name="screen_overlay_notif_message">Başlamağa hazır olanda yazma düyməsinə basın</string>
<string name="screen_settings_title">Ekran yazma tənzimləmələri</string>
......
......@@ -41,7 +41,6 @@
<string name="screen_notification_message_done">Готово за споделяне</string>
<string name="screen_recording_message">Екрана се записва\u2026</string>
<string name="screen_last_title">Последен запис</string>
<string name="screen_last_message">Дата: %1$s\nПродължителност: %2$d секунди</string>
<string name="screen_overlay_notif_title">Готово за запис</string>
<string name="screen_overlay_notif_message">Натиснете бутона за запис, когато сте готови да започнете</string>
<string name="screen_settings_title">Настройки за запис на екрана</string>
......
......@@ -41,7 +41,6 @@
<string name="screen_notification_message_done">A punt per compartir</string>
<string name="screen_recording_message">La pantalla està sent gravada\u2026</string>
<string name="screen_last_title">Darrer enregistrament</string>
<string name="screen_last_message">Data: %1$s\nDuració: %2$d segons</string>
<string name="screen_overlay_notif_title">Preparat per enregistrar</string>
<string name="screen_overlay_notif_message">Prem el botó de gravació quan estiguis a punt per començar</string>
<string name="screen_settings_title">Configuració de l\'enregistrador de pantalla</string>
......
......@@ -41,7 +41,6 @@
<string name="screen_notification_message_done">Připraven ke sdílení</string>
<string name="screen_recording_message">Obrazovka je zaznamenávána\u2026</string>
<string name="screen_last_title">Poslední záznam</string>
<string name="screen_last_message">Datum: %1$s\nTrvání: %2$d sekund</string>
<string name="screen_overlay_notif_title">Připraveno k záznamu</string>