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,49 +85,73 @@ 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);
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));
}
}
// 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 // Remove from list of ids
imageIds.remove(id); long fileId = cursor.getLong(0);
imageIds.remove(fileId);
try { files.add(new JSONObject()
JSONObject file = new JSONObject() .put(Fields.Photo.FILEID, fileId)
.put("fileid", id) .put(Fields.Photo.BASENAME, cursor.getString(1))
.put("basename", name) .put(Fields.Photo.MIMETYPE, cursor.getString(2))
.put("mimetype", mime) .put(Fields.Photo.HEIGHT, cursor.getLong(3))
.put("dayid", dayId) .put(Fields.Photo.WIDTH, cursor.getLong(4))
.put("datetaken", dateTaken) .put(Fields.Photo.SIZE, cursor.getLong(5))
.put("h", height) .put(Fields.Photo.ETAG, Long.toString(cursor.getLong(6)))
.put("w", width) .put(Fields.Photo.DATETAKEN, datesTaken.get(fileId))
.put("size", size) .put(Fields.Photo.DAYID, dayId)
.put("etag", dateModified.toString()); .put(Fields.Photo.ISVIDEO, 1)
files.add(file); .put(Fields.Photo.VIDEO_DURATION, cursor.getLong(7) / 1000));
} catch (JSONException e) {
Log.e(TAG, "JSON error");
}
} }
} }
@ -175,10 +187,9 @@ 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) }
)) { )) {
if (!cursor.moveToNext()) { if (!cursor.moveToNext()) {
throw new Exception("Image not found"); throw new Exception("Image not found");
@ -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,80 +234,57 @@ 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,
}; },
selection,
// Filter for given day null,
String selection = MediaStore.Images.Media._ID null
+ " = " + localId; )) {
if (!cursor.moveToNext()) {
try (Cursor cursor2 = mCtx.getContentResolver().query( throw new Exception("Image not found");
collection,
projection,
selection,
null,
null
)) {
int idColumn = cursor2.getColumnIndexOrThrow(MediaStore.Images.Media._ID);
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");
}
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()
.put("fileid", id2)
.put("basename", name)
.put("mimetype", mime)
.put("dayid", dayid)
.put("datetaken", dateTaken)
.put("h", height)
.put("w", width)
.put("size", size)
.put("permissions", "D");
// Get EXIF data
try {
ExifInterface exif = new ExifInterface(data);
JSONObject exifObj = new JSONObject();
exifObj.put("Aperture", exif.getAttribute(ExifInterface.TAG_APERTURE_VALUE));
exifObj.put("FocalLength", exif.getAttribute(ExifInterface.TAG_FOCAL_LENGTH));
exifObj.put("FNumber", exif.getAttribute(ExifInterface.TAG_F_NUMBER));
exifObj.put("ShutterSpeed", exif.getAttribute(ExifInterface.TAG_SHUTTER_SPEED_VALUE));
exifObj.put("ExposureTime", exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME));
exifObj.put("ISO", exif.getAttribute(ExifInterface.TAG_ISO_SPEED));
exifObj.put("DateTimeOriginal", exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
exifObj.put("OffsetTimeOriginal", exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL));
exifObj.put("GPSLatitude", exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE));
exifObj.put("GPSLongitude", exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE));
exifObj.put("GPSAltitude", exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE));
exifObj.put("Make", exif.getAttribute(ExifInterface.TAG_MAKE));
exifObj.put("Model", exif.getAttribute(ExifInterface.TAG_MODEL));
exifObj.put("Orientation", exif.getAttribute(ExifInterface.TAG_ORIENTATION));
exifObj.put("Description", exif.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION));
obj.put("exif", exifObj);
} catch (IOException e) {
Log.e(TAG, "Error reading EXIF data for " + data);
}
return obj;
} }
JSONObject obj = new JSONObject()
.put(Fields.Photo.FILEID, cursor.getLong(0))
.put(Fields.Photo.BASENAME, cursor.getString(1))
.put(Fields.Photo.MIMETYPE, cursor.getString(2))
.put(Fields.Photo.DAYID, dayId)
.put(Fields.Photo.DATETAKEN, dateTaken)
.put(Fields.Photo.HEIGHT, cursor.getLong(3))
.put(Fields.Photo.WIDTH, cursor.getLong(4))
.put(Fields.Photo.SIZE, cursor.getLong(5))
.put(Fields.Photo.PERMISSIONS, Fields.Perm.DELETE);
String uri = cursor.getString(6);
// Get EXIF data
try {
ExifInterface exif = new ExifInterface(uri);
JSONObject exifObj = new JSONObject();
exifObj.put("Aperture", exif.getAttribute(ExifInterface.TAG_APERTURE_VALUE));
exifObj.put("FocalLength", exif.getAttribute(ExifInterface.TAG_FOCAL_LENGTH));
exifObj.put("FNumber", exif.getAttribute(ExifInterface.TAG_F_NUMBER));
exifObj.put("ShutterSpeed", exif.getAttribute(ExifInterface.TAG_SHUTTER_SPEED_VALUE));
exifObj.put("ExposureTime", exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME));
exifObj.put("ISO", exif.getAttribute(ExifInterface.TAG_ISO_SPEED));
exifObj.put("DateTimeOriginal", exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
exifObj.put("OffsetTimeOriginal", exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL));
exifObj.put("GPSLatitude", exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE));
exifObj.put("GPSLongitude", exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE));
exifObj.put("GPSAltitude", exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE));
exifObj.put("Make", exif.getAttribute(ExifInterface.TAG_MAKE));
exifObj.put("Model", exif.getAttribute(ExifInterface.TAG_MODEL));
exifObj.put("Orientation", exif.getAttribute(ExifInterface.TAG_ORIENTATION));
exifObj.put("Description", exif.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION));
obj.put(Fields.Photo.EXIF, exifObj);
} catch (IOException e) {
Log.e(TAG, "Error reading EXIF data for " + uri);
}
return obj;
} }
} }
@ -324,82 +335,117 @@ 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[] {
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_TAKEN,
MediaStore.Images.Media.DATE_MODIFIED,
};
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.DATE_TAKEN,
MediaStore.Images.Media.DATE_MODIFIED,
MediaStore.Images.Media.DATA,
},
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),
// Check if file with local_id and mtime already exists cursor.getString(4),
try (Cursor c = mDb.rawQuery("SELECT id FROM images WHERE local_id = ?", false
new String[]{Long.toString(id)})) { );
if (c.getCount() > 0) {
// File already exists, remove flag
mDb.execSQL("UPDATE images SET flag = 0 WHERE local_id = ?", new Object[]{id});
Log.v(TAG, "File already exists: " + id + " / " + name);
continue;
}
}
// Get EXIF date using ExifInterface
String uri = cursor.getString(uriColumn);
try {
ExifInterface exif = new ExifInterface(uri);
String exifDate = exif.getAttribute(ExifInterface.TAG_DATETIME);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
sdf.setTimeZone(android.icu.util.TimeZone.GMT_ZONE);
dateTaken = sdf.parse(exifDate).getTime();
} catch (IOException e) {
Log.e(TAG, "Failed to read EXIF data: " + e.getMessage());
} catch (ParseException e) {
e.printStackTrace();
}
// This will use whatever is available
dateTaken /= 1000;
final long dayId = dateTaken / 86400;
// Delete file with same local_id and insert new one
mDb.beginTransaction();
mDb.execSQL("DELETE FROM images WHERE local_id = ?", new Object[] { id });
mDb.execSQL("INSERT OR IGNORE INTO images (local_id, mtime, basename, date_taken, dayid) VALUES (?, ?, ?, ?, ?)",
new Object[] { id, mtime, name, dateTaken, dayId });
mDb.setTransactionSuccessful();
mDb.endTransaction();
Log.v(TAG, "Inserted file to local DB: " + id + " / " + name + " / " + dayId);
} }
} }
// 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 // Clean up stale files
mDb.execSQL("DELETE FROM images WHERE flag = 1"); 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
try (Cursor c = mDb.rawQuery("SELECT id FROM images WHERE local_id = ?",
new String[]{Long.toString(id)})) {
if (c.getCount() > 0) {
// File already exists, remove flag
mDb.execSQL("UPDATE images SET flag = 0 WHERE local_id = ?", new Object[]{id});
Log.v(TAG, "File already exists: " + id + " / " + name);
return;
}
}
// Get EXIF date using ExifInterface if image
if (!isVideo) {
try {
ExifInterface exif = new ExifInterface(uri);
String exifDate = exif.getAttribute(ExifInterface.TAG_DATETIME);
if (exifDate == null) {
throw new IOException();
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
sdf.setTimeZone(android.icu.util.TimeZone.GMT_ZONE);
Date date = sdf.parse(exifDate);
if (date != null) {
dateTaken = date.getTime();
}
} catch (IOException e) {
Log.e(TAG, "Failed to read EXIF data: " + e.getMessage());
} catch (ParseException e) {
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
dateTaken /= 1000;
final long dayId = dateTaken / 86400;
// Delete file with same local_id and insert new one
mDb.beginTransaction();
mDb.execSQL("DELETE FROM images WHERE local_id = ?", new Object[] { id });
mDb.execSQL("INSERT OR IGNORE INTO images (local_id, mtime, basename, date_taken, dayid) VALUES (?, ?, ?, ?, ?)",
new Object[] { id, mtime, name, dateTaken, dayId });
mDb.setTransactionSuccessful();
mDb.endTransaction();
Log.v(TAG, "Inserted file to local DB: " + id + " / " + name + " / " + dayId);
}
} }