Show local videos in timeline

pull/653/merge
Varun Patil 2023-05-11 21:16:43 -07:00
parent bc5490ecdb
commit b0b0b74754
4 changed files with 273 additions and 195 deletions

View File

@ -6,7 +6,7 @@ import android.database.sqlite.SQLiteOpenHelper;
public class DbService extends SQLiteOpenHelper { public class DbService extends SQLiteOpenHelper {
public DbService(Context context) { public DbService(Context context) {
super(context, "memories", null, 16); super(context, "memories", null, 24);
} }
public void onCreate(SQLiteDatabase db) { public void onCreate(SQLiteDatabase db) {

View File

@ -0,0 +1,27 @@
package gallery.memories.service;
public class Fields {
public final static class Photo {
public static final String FILEID = "fileid";
public static final String BASENAME = "basename";
public static final String MIMETYPE = "mimetype";
public static final String HEIGHT = "h";
public static final String WIDTH = "w";
public static final String SIZE = "size";
public static final String ETAG = "etag";
public static final String DATETAKEN = "datetaken";
public static final String DAYID = "dayid";
public static final String ISVIDEO = "isvideo";
public static final String VIDEO_DURATION = "video_duration";
public static final String EXIF = "exif";
public static final String PERMISSIONS = "permissions";
}
public final static class Perm {
public static final String DELETE = "D";
}
public final static class Exif {
}
}

View File

@ -16,7 +16,12 @@ public class ImageService {
public byte[] getPreview(final long id) throws Exception { public byte[] getPreview(final long id) throws Exception {
Bitmap bitmap = MediaStore.Images.Thumbnails.getThumbnail( Bitmap bitmap = MediaStore.Images.Thumbnails.getThumbnail(
mCtx.getContentResolver(), id, MediaStore.Images.Thumbnails.MINI_KIND, null); mCtx.getContentResolver(), id, MediaStore.Images.Thumbnails.FULL_SCREEN_KIND, null);
if (bitmap == null) {
bitmap = MediaStore.Video.Thumbnails.getThumbnail(
mCtx.getContentResolver(), id, MediaStore.Video.Thumbnails.FULL_SCREEN_KIND, null);
}
if (bitmap == null) { if (bitmap == null) {
throw new Exception("Thumbnail not found"); throw new Exception("Thumbnail not found");

View File

@ -6,6 +6,7 @@ import android.content.ContentUris;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.icu.text.SimpleDateFormat; import android.icu.text.SimpleDateFormat;
import android.icu.util.TimeZone;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.provider.MediaStore; import android.provider.MediaStore;
@ -27,6 +28,7 @@ import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.text.ParseException; import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -55,7 +57,7 @@ public class TimelineQuery {
fullSyncDb(); fullSyncDb();
} }
public JSONArray getByDayId(final long dayId) { public JSONArray getByDayId(final long dayId) throws JSONException {
// Get list of images from DB // Get list of images from DB
final Set<Long> imageIds = new ArraySet<>(); final Set<Long> imageIds = new ArraySet<>();
final Map<Long, Long> datesTaken = new HashMap<>(); final Map<Long, Long> datesTaken = new HashMap<>();
@ -76,20 +78,6 @@ public class TimelineQuery {
return new JSONArray(); return new JSONArray();
} }
// All external storage images
Uri collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
// Same fields as server response
String[] projection = new String[] {
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.MIME_TYPE,
MediaStore.Images.Media.HEIGHT,
MediaStore.Images.Media.WIDTH,
MediaStore.Images.Media.SIZE,
MediaStore.Images.Media.DATE_MODIFIED,
};
// Filter for given day // Filter for given day
String selection = MediaStore.Images.Media._ID String selection = MediaStore.Images.Media._ID
+ " IN (" + TextUtils.join(",", imageIds) + ")"; + " IN (" + TextUtils.join(",", imageIds) + ")";
@ -97,50 +85,74 @@ public class TimelineQuery {
// Make list of files // Make list of files
ArrayList<JSONObject> files = new ArrayList<>(); ArrayList<JSONObject> files = new ArrayList<>();
// Add all images
try (Cursor cursor = mCtx.getContentResolver().query( try (Cursor cursor = mCtx.getContentResolver().query(
collection, MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection, new String[] {
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.MIME_TYPE,
MediaStore.Images.Media.HEIGHT,
MediaStore.Images.Media.WIDTH,
MediaStore.Images.Media.SIZE,
MediaStore.Images.Media.DATE_MODIFIED,
},
selection, selection,
null, null,
null null
)) { )) {
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID);
int nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME);
int mimeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE);
int heightColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.HEIGHT);
int widthColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.WIDTH);
int sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE);
int dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED);
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
long id = cursor.getLong(idColumn); long fileId = cursor.getLong(0);
String name = cursor.getString(nameColumn); imageIds.remove(fileId);
String mime = cursor.getString(mimeColumn);
long height = cursor.getLong(heightColumn);
long width = cursor.getLong(widthColumn);
long size = cursor.getLong(sizeColumn);
long dateTaken = datesTaken.get(id);
Long dateModified = cursor.getLong(dateModifiedColumn);
// Remove from list of ids files.add(new JSONObject()
imageIds.remove(id); .put(Fields.Photo.FILEID, fileId)
.put(Fields.Photo.BASENAME, cursor.getString(1))
try { .put(Fields.Photo.MIMETYPE, cursor.getString(2))
JSONObject file = new JSONObject() .put(Fields.Photo.HEIGHT, cursor.getLong(3))
.put("fileid", id) .put(Fields.Photo.WIDTH, cursor.getLong(4))
.put("basename", name) .put(Fields.Photo.SIZE, cursor.getLong(5))
.put("mimetype", mime) .put(Fields.Photo.ETAG, Long.toString(cursor.getLong(6)))
.put("dayid", dayId) .put(Fields.Photo.DATETAKEN, datesTaken.get(fileId))
.put("datetaken", dateTaken) .put(Fields.Photo.DAYID, dayId));
.put("h", height)
.put("w", width)
.put("size", size)
.put("etag", dateModified.toString());
files.add(file);
} catch (JSONException e) {
Log.e(TAG, "JSON error");
} }
} }
// Add all videos
try (Cursor cursor = mCtx.getContentResolver().query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
new String[] {
MediaStore.Video.Media._ID,
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.MIME_TYPE,
MediaStore.Video.Media.HEIGHT,
MediaStore.Video.Media.WIDTH,
MediaStore.Video.Media.SIZE,
MediaStore.Video.Media.DATE_MODIFIED,
MediaStore.Video.Media.DURATION,
},
selection,
null,
null
)) {
while (cursor.moveToNext()) {
// Remove from list of ids
long fileId = cursor.getLong(0);
imageIds.remove(fileId);
files.add(new JSONObject()
.put(Fields.Photo.FILEID, fileId)
.put(Fields.Photo.BASENAME, cursor.getString(1))
.put(Fields.Photo.MIMETYPE, cursor.getString(2))
.put(Fields.Photo.HEIGHT, cursor.getLong(3))
.put(Fields.Photo.WIDTH, cursor.getLong(4))
.put(Fields.Photo.SIZE, cursor.getLong(5))
.put(Fields.Photo.ETAG, Long.toString(cursor.getLong(6)))
.put(Fields.Photo.DATETAKEN, datesTaken.get(fileId))
.put(Fields.Photo.DAYID, dayId)
.put(Fields.Photo.ISVIDEO, 1)
.put(Fields.Photo.VIDEO_DURATION, cursor.getLong(7) / 1000));
}
} }
// Remove files that were not found // Remove files that were not found
@ -175,7 +187,6 @@ public class TimelineQuery {
} }
public JSONObject getImageInfo(final long id) throws Exception { public JSONObject getImageInfo(final long id) throws Exception {
// Get image info from DB
try (Cursor cursor = mDb.rawQuery( try (Cursor cursor = mDb.rawQuery(
"SELECT local_id, date_taken, dayid FROM images WHERE local_id = ?", "SELECT local_id, date_taken, dayid FROM images WHERE local_id = ?",
new String[] { Long.toString(id) } new String[] { Long.toString(id) }
@ -186,13 +197,36 @@ public class TimelineQuery {
final long localId = cursor.getLong(0); final long localId = cursor.getLong(0);
final long dateTaken = cursor.getLong(1); final long dateTaken = cursor.getLong(1);
final long dayid = cursor.getLong(2); final long dayId = cursor.getLong(2);
// All external storage images try {
Uri collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; return _getImageInfoForCollection(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
localId, dateTaken, dayId
);
} catch (Exception e) {/* Ignore */}
// Same fields as server response try {
String[] projection = new String[] { return _getImageInfoForCollection(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
localId, dateTaken, dayId
);
} catch (Exception e) {/* Ignore */}
}
throw new Exception("File not found in any collection");
}
public JSONObject _getImageInfoForCollection(
final Uri collection,
final long localId,
final long dateTaken,
final long dayId
) throws Exception {
String selection = MediaStore.Images.Media._ID + " = " + localId;
try (Cursor cursor = mCtx.getContentResolver().query(
collection,
new String[] {
MediaStore.Images.Media._ID, MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.MIME_TYPE, MediaStore.Images.Media.MIME_TYPE,
@ -200,53 +234,31 @@ public class TimelineQuery {
MediaStore.Images.Media.WIDTH, MediaStore.Images.Media.WIDTH,
MediaStore.Images.Media.SIZE, MediaStore.Images.Media.SIZE,
MediaStore.Images.Media.DATA, MediaStore.Images.Media.DATA,
}; },
// Filter for given day
String selection = MediaStore.Images.Media._ID
+ " = " + localId;
try (Cursor cursor2 = mCtx.getContentResolver().query(
collection,
projection,
selection, selection,
null, null,
null null
)) { )) {
int idColumn = cursor2.getColumnIndexOrThrow(MediaStore.Images.Media._ID); if (!cursor.moveToNext()) {
int nameColumn = cursor2.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME);
int mimeColumn = cursor2.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE);
int heightColumn = cursor2.getColumnIndexOrThrow(MediaStore.Images.Media.HEIGHT);
int widthColumn = cursor2.getColumnIndexOrThrow(MediaStore.Images.Media.WIDTH);
int sizeColumn = cursor2.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE);
int dataColumn = cursor2.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
if (!cursor2.moveToNext()) {
throw new Exception("Image not found"); throw new Exception("Image not found");
} }
long id2 = cursor2.getLong(idColumn);
String name = cursor2.getString(nameColumn);
String mime = cursor2.getString(mimeColumn);
long height = cursor2.getLong(heightColumn);
long width = cursor2.getLong(widthColumn);
long size = cursor2.getLong(sizeColumn);
String data = cursor2.getString(dataColumn);
JSONObject obj = new JSONObject() JSONObject obj = new JSONObject()
.put("fileid", id2) .put(Fields.Photo.FILEID, cursor.getLong(0))
.put("basename", name) .put(Fields.Photo.BASENAME, cursor.getString(1))
.put("mimetype", mime) .put(Fields.Photo.MIMETYPE, cursor.getString(2))
.put("dayid", dayid) .put(Fields.Photo.DAYID, dayId)
.put("datetaken", dateTaken) .put(Fields.Photo.DATETAKEN, dateTaken)
.put("h", height) .put(Fields.Photo.HEIGHT, cursor.getLong(3))
.put("w", width) .put(Fields.Photo.WIDTH, cursor.getLong(4))
.put("size", size) .put(Fields.Photo.SIZE, cursor.getLong(5))
.put("permissions", "D"); .put(Fields.Photo.PERMISSIONS, Fields.Perm.DELETE);
String uri = cursor.getString(6);
// Get EXIF data // Get EXIF data
try { try {
ExifInterface exif = new ExifInterface(data); ExifInterface exif = new ExifInterface(uri);
JSONObject exifObj = new JSONObject(); JSONObject exifObj = new JSONObject();
exifObj.put("Aperture", exif.getAttribute(ExifInterface.TAG_APERTURE_VALUE)); exifObj.put("Aperture", exif.getAttribute(ExifInterface.TAG_APERTURE_VALUE));
exifObj.put("FocalLength", exif.getAttribute(ExifInterface.TAG_FOCAL_LENGTH)); exifObj.put("FocalLength", exif.getAttribute(ExifInterface.TAG_FOCAL_LENGTH));
@ -267,15 +279,14 @@ public class TimelineQuery {
exifObj.put("Orientation", exif.getAttribute(ExifInterface.TAG_ORIENTATION)); exifObj.put("Orientation", exif.getAttribute(ExifInterface.TAG_ORIENTATION));
exifObj.put("Description", exif.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION)); exifObj.put("Description", exif.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION));
obj.put("exif", exifObj); obj.put(Fields.Photo.EXIF, exifObj);
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Error reading EXIF data for " + data); Log.e(TAG, "Error reading EXIF data for " + uri);
} }
return obj; return obj;
} }
} }
}
public JSONObject delete(List<Long> ids) throws Exception { public JSONObject delete(List<Long> ids) throws Exception {
synchronized (this) { synchronized (this) {
@ -324,39 +335,67 @@ public class TimelineQuery {
} }
protected void fullSyncDb() { protected void fullSyncDb() {
Uri collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
// Flag all images for removal // Flag all images for removal
mDb.execSQL("UPDATE images SET flag = 1"); mDb.execSQL("UPDATE images SET flag = 1");
// Same fields as server response // Add all images
String[] projection = new String[] { try (Cursor cursor = mCtx.getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[] {
MediaStore.Images.Media._ID, MediaStore.Images.Media._ID,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_TAKEN, MediaStore.Images.Media.DATE_TAKEN,
MediaStore.Images.Media.DATE_MODIFIED, MediaStore.Images.Media.DATE_MODIFIED,
}; MediaStore.Images.Media.DATA,
},
try (Cursor cursor = mCtx.getContentResolver().query(
collection,
projection,
null, null,
null, null,
null null
)) { )) {
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID);
int uriColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
int nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME);
int dateColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_TAKEN);
int mtimeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED);
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
long id = cursor.getLong(idColumn); insertItemDb(
String name = cursor.getString(nameColumn); cursor.getLong(0),
long dateTaken = cursor.getLong(dateColumn); cursor.getString(1),
long mtime = cursor.getLong(mtimeColumn); cursor.getLong(2),
cursor.getLong(3),
cursor.getString(4),
false
);
}
}
// Add all videos
try (Cursor cursor = mCtx.getContentResolver().query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
new String[] {
MediaStore.Video.Media._ID,
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.DATE_TAKEN,
MediaStore.Video.Media.DATE_MODIFIED,
MediaStore.Video.Media.DATA,
},
null,
null,
null
)) {
while (cursor.moveToNext()) {
insertItemDb(
cursor.getLong(0),
cursor.getString(1),
cursor.getLong(2),
cursor.getLong(3),
cursor.getString(4),
true
);
}
}
// Clean up stale files
mDb.execSQL("DELETE FROM images WHERE flag = 1");
}
protected void insertItemDb(long id, String name, long dateTaken, long mtime, String uri, boolean isVideo) {
// Check if file with local_id and mtime already exists // Check if file with local_id and mtime already exists
try (Cursor c = mDb.rawQuery("SELECT id FROM images WHERE local_id = ?", try (Cursor c = mDb.rawQuery("SELECT id FROM images WHERE local_id = ?",
new String[]{Long.toString(id)})) { new String[]{Long.toString(id)})) {
@ -365,23 +404,35 @@ public class TimelineQuery {
mDb.execSQL("UPDATE images SET flag = 0 WHERE local_id = ?", new Object[]{id}); mDb.execSQL("UPDATE images SET flag = 0 WHERE local_id = ?", new Object[]{id});
Log.v(TAG, "File already exists: " + id + " / " + name); Log.v(TAG, "File already exists: " + id + " / " + name);
continue; return;
} }
} }
// Get EXIF date using ExifInterface // Get EXIF date using ExifInterface if image
String uri = cursor.getString(uriColumn); if (!isVideo) {
try { try {
ExifInterface exif = new ExifInterface(uri); ExifInterface exif = new ExifInterface(uri);
String exifDate = exif.getAttribute(ExifInterface.TAG_DATETIME); String exifDate = exif.getAttribute(ExifInterface.TAG_DATETIME);
if (exifDate == null) {
throw new IOException();
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
sdf.setTimeZone(android.icu.util.TimeZone.GMT_ZONE); sdf.setTimeZone(android.icu.util.TimeZone.GMT_ZONE);
dateTaken = sdf.parse(exifDate).getTime(); Date date = sdf.parse(exifDate);
if (date != null) {
dateTaken = date.getTime();
}
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Failed to read EXIF data: " + e.getMessage()); Log.e(TAG, "Failed to read EXIF data: " + e.getMessage());
} catch (ParseException e) { } catch (ParseException e) {
e.printStackTrace(); e.printStackTrace();
} }
}
if (isVideo) {
// No way to get the actual local date, so just assume current timezone
dateTaken += TimeZone.getDefault().getOffset(dateTaken);
}
// This will use whatever is available // This will use whatever is available
dateTaken /= 1000; dateTaken /= 1000;
@ -397,9 +448,4 @@ public class TimelineQuery {
Log.v(TAG, "Inserted file to local DB: " + id + " / " + name + " / " + dayId); Log.v(TAG, "Inserted file to local DB: " + id + " / " + name + " / " + dayId);
} }
}
// Clean up stale files
mDb.execSQL("DELETE FROM images WHERE flag = 1");
}
} }