From 39f2af8dc3a96e1a01dc76deb61a334044f1ed43 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 12 May 2023 01:16:30 -0700 Subject: [PATCH] Convert to kotlin --- .../java/gallery/memories/MainActivity.java | 60 --- .../java/gallery/memories/MainActivity.kt | 53 ++ .../main/java/gallery/memories/NativeX.java | 149 ------ app/src/main/java/gallery/memories/NativeX.kt | 140 ++++++ .../gallery/memories/service/DbService.java | 30 -- .../gallery/memories/service/DbService.kt | 27 ++ .../service/DownloadBroadcastReceiver.kt | 2 +- .../memories/service/DownloadService.kt | 2 +- .../memories/service/ImageService.java | 48 -- .../gallery/memories/service/ImageService.kt | 35 ++ .../memories/service/TimelineQuery.java | 451 ------------------ .../gallery/memories/service/TimelineQuery.kt | 415 ++++++++++++++++ 12 files changed, 672 insertions(+), 740 deletions(-) delete mode 100644 app/src/main/java/gallery/memories/MainActivity.java create mode 100644 app/src/main/java/gallery/memories/MainActivity.kt delete mode 100644 app/src/main/java/gallery/memories/NativeX.java create mode 100644 app/src/main/java/gallery/memories/NativeX.kt delete mode 100644 app/src/main/java/gallery/memories/service/DbService.java create mode 100644 app/src/main/java/gallery/memories/service/DbService.kt delete mode 100644 app/src/main/java/gallery/memories/service/ImageService.java create mode 100644 app/src/main/java/gallery/memories/service/ImageService.kt delete mode 100644 app/src/main/java/gallery/memories/service/TimelineQuery.java create mode 100644 app/src/main/java/gallery/memories/service/TimelineQuery.kt diff --git a/app/src/main/java/gallery/memories/MainActivity.java b/app/src/main/java/gallery/memories/MainActivity.java deleted file mode 100644 index f76476c9..00000000 --- a/app/src/main/java/gallery/memories/MainActivity.java +++ /dev/null @@ -1,60 +0,0 @@ -package gallery.memories; - -import android.annotation.SuppressLint; -import android.os.Bundle; -import android.webkit.WebResourceRequest; -import android.webkit.WebResourceResponse; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.webkit.WebViewClient; - -import androidx.appcompat.app.AppCompatActivity; - -import gallery.memories.databinding.ActivityMainBinding; - -public class MainActivity extends AppCompatActivity { - public static final String TAG = "memories-native"; - protected ActivityMainBinding binding; - protected NativeX mNativeX; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - binding = ActivityMainBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - mNativeX = new NativeX(this, binding.webview); - initializeWebView(); - } - - @SuppressLint("SetJavaScriptEnabled") - protected void initializeWebView() { - binding.webview.setWebViewClient(new WebViewClient() { - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - view.loadUrl(request.getUrl().toString()); - return false; - } - - @Override - public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { - if (request.getUrl().getHost().equals("127.0.0.1")) { - return mNativeX.handleRequest(request); - } - return null; - } - }); - - WebSettings webSettings = binding.webview.getSettings(); - webSettings.setJavaScriptEnabled(true); - webSettings.setJavaScriptCanOpenWindowsAutomatically(true); - webSettings.setAllowContentAccess(true); - webSettings.setDomStorageEnabled(true); - webSettings.setDatabaseEnabled(true); - webSettings.setUserAgentString("memories-native-android/0.0"); - - binding.webview.clearCache(true); - binding.webview.addJavascriptInterface(mNativeX, "nativex"); - binding.webview.loadUrl("http://10.0.2.2:8035/index.php/apps/memories/"); - } -} \ No newline at end of file diff --git a/app/src/main/java/gallery/memories/MainActivity.kt b/app/src/main/java/gallery/memories/MainActivity.kt new file mode 100644 index 00000000..105ed09f --- /dev/null +++ b/app/src/main/java/gallery/memories/MainActivity.kt @@ -0,0 +1,53 @@ +package gallery.memories + +import android.annotation.SuppressLint +import android.os.Bundle +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.appcompat.app.AppCompatActivity +import gallery.memories.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + private lateinit var mNativeX: NativeX + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + // Initialize services + mNativeX = NativeX(this) + + // Load JavaScript + initializeWebView() + } + + @SuppressLint("SetJavaScriptEnabled") + protected fun initializeWebView() { + binding.webview.webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { + view.loadUrl(request.url.toString()) + return false + } + + override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { + return if (request.url.host == "127.0.0.1") { + mNativeX.handleRequest(request) + } else null + } + } + val webSettings = binding.webview.settings + webSettings.javaScriptEnabled = true + webSettings.javaScriptCanOpenWindowsAutomatically = true + webSettings.allowContentAccess = true + webSettings.domStorageEnabled = true + webSettings.databaseEnabled = true + webSettings.userAgentString = "memories-native-android/0.0" + binding.webview.clearCache(true) + binding.webview.addJavascriptInterface(mNativeX, "nativex") + binding.webview.loadUrl("http://10.0.2.2:8035/index.php/apps/memories/") + } +} \ No newline at end of file diff --git a/app/src/main/java/gallery/memories/NativeX.java b/app/src/main/java/gallery/memories/NativeX.java deleted file mode 100644 index 4ce1a016..00000000 --- a/app/src/main/java/gallery/memories/NativeX.java +++ /dev/null @@ -1,149 +0,0 @@ -package gallery.memories; - -import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; - -import android.graphics.Color; -import android.os.Build; -import android.util.Log; -import android.view.View; -import android.view.Window; -import android.webkit.JavascriptInterface; -import android.webkit.WebResourceRequest; -import android.webkit.WebResourceResponse; -import android.webkit.WebView; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.collection.ArrayMap; - -import java.io.ByteArrayInputStream; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import gallery.memories.service.DownloadService; -import gallery.memories.service.ImageService; -import gallery.memories.service.TimelineQuery; - -public class NativeX { - public static final String TAG = "NativeX"; - protected final AppCompatActivity mActivity; - protected final WebView mWebView; - - protected final ImageService mImageService; - protected final TimelineQuery mQuery; - public static DownloadService mDlService; - - public NativeX(AppCompatActivity activity, WebView webView) { - mActivity = activity; - mWebView = webView; - mImageService = new ImageService(activity); - mQuery = new TimelineQuery(activity); - mDlService = new DownloadService(activity); - } - - public WebResourceResponse handleRequest(final WebResourceRequest request) { - final String path = request.getUrl().getPath(); - - WebResourceResponse response; - try { - if (request.getMethod().equals("GET")) { - response = routerGet(path); - } else if (request.getMethod().equals("OPTIONS")) { - response = new WebResourceResponse("text/plain", "UTF-8", new ByteArrayInputStream("".getBytes())); - } else { - throw new Exception("Method Not Allowed"); - } - } catch (Exception e) { - Log.e(TAG, "handleRequest: ", e); - response = makeErrorResponse(); - } - - // Allow CORS from all origins - Map headers = new ArrayMap<>(); - headers.put("Access-Control-Allow-Origin", "*"); - headers.put("Access-Control-Allow-Headers", "*"); - response.setResponseHeaders(headers); - - return response; - } - - @JavascriptInterface - public boolean isNative() { - return true; - } - - @JavascriptInterface - public void setThemeColor(final String color, final boolean isDark) { - Window window = mActivity.getWindow(); - - mActivity.setTheme(isDark - ? android.R.style.Theme_Black - : android.R.style.Theme_Light); - window.setNavigationBarColor(Color.parseColor(color)); - window.setStatusBarColor(Color.parseColor(color)); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - window.getInsetsController().setSystemBarsAppearance(isDark ? 0 : APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS); - } else { - window.getDecorView().setSystemUiVisibility(isDark ? 0 : View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); - } - } - - @JavascriptInterface - public void downloadFromUrl(final String url, final String filename) { - mDlService.queue(url, filename); - } - - protected WebResourceResponse routerGet(final String path) throws Exception { - String[] parts = path.split("/"); - - if (path.matches("^/image/preview/\\d+$")) { - return makeResponse(mImageService.getPreview(Long.parseLong(parts[3])), "image/jpeg"); - } else if (path.matches("^/image/full/\\d+$")) { - return makeResponse(mImageService.getFull(Long.parseLong(parts[3])), "image/jpeg"); - } else if (path.matches("^/api/image/info/\\d+$")) { - return makeResponse(mQuery.getImageInfo(Long.parseLong(parts[4]))); - } else if (path.matches("^/api/image/delete/\\d+(,\\d+)*$")) { - return makeResponse(mQuery.delete(parseIds(parts[4]))); - } else if (path.matches("^/api/days$")) { - return makeResponse(mQuery.getDays()); - } else if (path.matches("/api/days/\\d+$")) { - return makeResponse(mQuery.getByDayId(Long.parseLong(parts[3]))); - } else if (path.matches("/api/share/url/.+$")) { - return makeResponse(mDlService.shareUrl(URLDecoder.decode(parts[4], "UTF-8"))); - } else if (path.matches("/api/share/blob/.+$")) { - return makeResponse(mDlService.shareBlobFromUrl(URLDecoder.decode(parts[4], "UTF-8"))); - } else if (path.matches("/api/share/local/\\d+$")) { - return makeResponse(mDlService.shareLocal(Long.parseLong(parts[4]))); - } - - throw new Exception("Not Found"); - } - - protected WebResourceResponse makeResponse(byte[] bytes, String mimeType) { - if (bytes != null) { - return new WebResourceResponse(mimeType, "UTF-8", new ByteArrayInputStream(bytes)); - } - - return makeErrorResponse(); - } - - protected WebResourceResponse makeResponse(Object json) { - return makeResponse(json.toString().getBytes(), "application/json"); - } - - protected WebResourceResponse makeErrorResponse() { - WebResourceResponse response = new WebResourceResponse("application/json", "UTF-8", new ByteArrayInputStream("{}".getBytes())); - response.setStatusCodeAndReasonPhrase(500, "Internal Server Error"); - return response; - } - - protected static List parseIds(String ids) { - List result = new ArrayList<>(); - for (String id : ids.split(",")) { - result.add(Long.parseLong(id)); - } - return result; - } -} diff --git a/app/src/main/java/gallery/memories/NativeX.kt b/app/src/main/java/gallery/memories/NativeX.kt new file mode 100644 index 00000000..ef3395bd --- /dev/null +++ b/app/src/main/java/gallery/memories/NativeX.kt @@ -0,0 +1,140 @@ +package gallery.memories + +import android.R +import android.graphics.Color +import android.os.Build +import android.util.Log +import android.view.View +import android.view.WindowInsetsController +import android.webkit.JavascriptInterface +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import androidx.appcompat.app.AppCompatActivity +import gallery.memories.service.DownloadService +import gallery.memories.service.ImageService +import gallery.memories.service.TimelineQuery +import java.io.ByteArrayInputStream +import java.net.URLDecoder + +class NativeX(private val mActivity: AppCompatActivity) { + val TAG = "NativeX" + + private val mImageService: ImageService = ImageService(mActivity) + private val mQuery: TimelineQuery = TimelineQuery(mActivity) + + object API { + val DAYS = Regex("^/api/days$") + val DAY = Regex("^/api/days/\\d+$") + val IMAGE_INFO = Regex("^/api/image/info/\\d+$") + val IMAGE_DELETE = Regex("^/api/image/delete/\\d+(,\\d+)*$") + + val IMAGE_PREVIEW = Regex("^/image/preview/\\d+$") + val IMAGE_FULL = Regex("^/image/full/\\d+$") + + val SHARE_URL = Regex("/api/share/url/.+$") + val SHARE_BLOB = Regex("/api/share/blob/.+$") + val SHARE_LOCAL = Regex("/api/share/local/\\d+$") + } + + init { + mDlService = DownloadService(mActivity) + } + + companion object { + lateinit var mDlService: DownloadService + fun getDlService(): DownloadService { + return mDlService; + } + } + + fun handleRequest(request: WebResourceRequest): WebResourceResponse { + val path = request.url.path + val response: WebResourceResponse = try { + if (request.method == "GET") { + routerGet(path) + } else if (request.method == "OPTIONS") { + WebResourceResponse("text/plain", "UTF-8", ByteArrayInputStream("".toByteArray())) + } else { + throw Exception("Method Not Allowed") + } + } catch (e: Exception) { + Log.e(TAG, "handleRequest: ", e) + makeErrorResponse() + } + + // Allow CORS from all origins + response.responseHeaders = mapOf( + "Access-Control-Allow-Origin" to "*", + "Access-Control-Allow-Headers" to "*" + ) + return response + } + + @get:JavascriptInterface + val isNative: Boolean + get() = true + + @JavascriptInterface + fun setThemeColor(color: String?, isDark: Boolean) { + val window = mActivity.window + mActivity.setTheme(if (isDark) R.style.Theme_Black else R.style.Theme_Light) + window.navigationBarColor = Color.parseColor(color) + window.statusBarColor = Color.parseColor(color) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.insetsController?.setSystemBarsAppearance(if (isDark) 0 else WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS) + } else { + window.decorView.systemUiVisibility = if (isDark) 0 else View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } + } + + @JavascriptInterface + fun downloadFromUrl(url: String?, filename: String?) { + mDlService.queue(url!!, filename!!) + } + + @Throws(Exception::class) + private fun routerGet(path: String?): WebResourceResponse { + val parts = path!!.split("/").toTypedArray() + if (path.matches(API.IMAGE_PREVIEW)) { + return makeResponse(mImageService.getPreview(parts[3].toLong()), "image/jpeg") + } else if (path.matches(API.IMAGE_FULL)) { + return makeResponse(mImageService.getFull(parts[3].toLong()), "image/jpeg") + } else if (path.matches(API.IMAGE_INFO)) { + return makeResponse(mQuery.getImageInfo(parts[4].toLong())) + } else if (path.matches(API.IMAGE_DELETE)) { + return makeResponse(mQuery.delete(parseIds(parts[4]))) + } else if (path.matches(API.DAYS)) { + return makeResponse(mQuery.getDays()) + } else if (path.matches(API.DAY)) { + return makeResponse(mQuery.getByDayId(parts[3].toLong())) + } else if (path.matches(API.SHARE_URL)) { + return makeResponse(mDlService.shareUrl(URLDecoder.decode(parts[4], "UTF-8"))) + } else if (path.matches(API.SHARE_BLOB)) { + return makeResponse(mDlService.shareBlobFromUrl(URLDecoder.decode(parts[4], "UTF-8"))) + } else if (path.matches(API.SHARE_LOCAL)) { + return makeResponse(mDlService.shareLocal(parts[4].toLong())) + } else { + throw Exception("Not Found") + } + } + + private fun makeResponse(bytes: ByteArray?, mimeType: String?): WebResourceResponse { + return if (bytes != null) { + WebResourceResponse(mimeType, "UTF-8", ByteArrayInputStream(bytes)) + } else makeErrorResponse() + } + + private fun makeResponse(json: Any): WebResourceResponse { + return makeResponse(json.toString().toByteArray(), "application/json") + } + + private fun makeErrorResponse(): WebResourceResponse { + val response = WebResourceResponse("application/json", "UTF-8", ByteArrayInputStream("{}".toByteArray())) + response.setStatusCodeAndReasonPhrase(500, "Internal Server Error") + return response + } + + private fun parseIds(ids: String): List { + return ids.split(",").map { it.toLong() } + } +} \ No newline at end of file diff --git a/app/src/main/java/gallery/memories/service/DbService.java b/app/src/main/java/gallery/memories/service/DbService.java deleted file mode 100644 index 607e1a49..00000000 --- a/app/src/main/java/gallery/memories/service/DbService.java +++ /dev/null @@ -1,30 +0,0 @@ -package gallery.memories.service; - -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; - -public class DbService extends SQLiteOpenHelper { - public DbService(Context context) { - super(context, "memories", null, 24); - } - - public void onCreate(SQLiteDatabase db) { - // Add table for images - db.execSQL("CREATE TABLE images (" - + "id INTEGER PRIMARY KEY AUTOINCREMENT," - + "local_id INTEGER," - + "mtime INTEGER," - + "date_taken INTEGER," - + "dayid INTEGER," - + "exif_uid TEXT," - + "basename TEXT," - + "flag INTEGER" - + ");"); - } - - public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) { - database.execSQL("DROP TABLE IF EXISTS images"); - onCreate(database); - } -} \ No newline at end of file diff --git a/app/src/main/java/gallery/memories/service/DbService.kt b/app/src/main/java/gallery/memories/service/DbService.kt new file mode 100644 index 00000000..321a16f5 --- /dev/null +++ b/app/src/main/java/gallery/memories/service/DbService.kt @@ -0,0 +1,27 @@ +package gallery.memories.service + +import android.content.Context +import android.database.sqlite.SQLiteOpenHelper +import android.database.sqlite.SQLiteDatabase + +class DbService(context: Context) : SQLiteOpenHelper(context, "memories", null, 25) { + override fun onCreate(db: SQLiteDatabase) { + db.execSQL(""" + CREATE TABLE images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + local_id INTEGER, + mtime INTEGER, + date_taken INTEGER, + dayid INTEGER, + exif_uid TEXT, + basename TEXT, + flag INTEGER + ) + """) + } + + override fun onUpgrade(database: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + database.execSQL("DROP TABLE IF EXISTS images") + onCreate(database) + } +} \ No newline at end of file diff --git a/app/src/main/java/gallery/memories/service/DownloadBroadcastReceiver.kt b/app/src/main/java/gallery/memories/service/DownloadBroadcastReceiver.kt index 1b0183ae..c918ac71 100644 --- a/app/src/main/java/gallery/memories/service/DownloadBroadcastReceiver.kt +++ b/app/src/main/java/gallery/memories/service/DownloadBroadcastReceiver.kt @@ -7,6 +7,6 @@ import gallery.memories.NativeX class DownloadBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - NativeX.mDlService?.runDownloadCallback(intent) + NativeX.getDlService().runDownloadCallback(intent) } } \ No newline at end of file diff --git a/app/src/main/java/gallery/memories/service/DownloadService.kt b/app/src/main/java/gallery/memories/service/DownloadService.kt index 4ab083db..95e568c3 100644 --- a/app/src/main/java/gallery/memories/service/DownloadService.kt +++ b/app/src/main/java/gallery/memories/service/DownloadService.kt @@ -13,7 +13,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.collection.ArrayMap import java.util.concurrent.CountDownLatch -class DownloadService(val mActivity: AppCompatActivity) { +class DownloadService(private val mActivity: AppCompatActivity) { private val mDownloads: MutableMap Unit> = ArrayMap() fun runDownloadCallback(intent: Intent) { diff --git a/app/src/main/java/gallery/memories/service/ImageService.java b/app/src/main/java/gallery/memories/service/ImageService.java deleted file mode 100644 index c18bf6b3..00000000 --- a/app/src/main/java/gallery/memories/service/ImageService.java +++ /dev/null @@ -1,48 +0,0 @@ -package gallery.memories.service; - -import android.content.ContentUris; -import android.content.Context; -import android.graphics.Bitmap; -import android.provider.MediaStore; - -import java.io.ByteArrayOutputStream; - -public class ImageService { - Context mCtx; - - public ImageService(Context context) { - mCtx = context; - } - - public byte[] getPreview(final long id) throws Exception { - Bitmap bitmap = MediaStore.Images.Thumbnails.getThumbnail( - 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) { - throw new Exception("Thumbnail not found"); - } - - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream); - return stream.toByteArray(); - } - - public byte[] getFull(final long id) throws Exception { - Bitmap bitmap = MediaStore.Images.Media.getBitmap( - mCtx.getContentResolver(), ContentUris.withAppendedId( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)); - - if (bitmap == null) { - throw new Exception("Image not found"); - } - - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream); - return stream.toByteArray(); - } -} diff --git a/app/src/main/java/gallery/memories/service/ImageService.kt b/app/src/main/java/gallery/memories/service/ImageService.kt new file mode 100644 index 00000000..d0d01a7c --- /dev/null +++ b/app/src/main/java/gallery/memories/service/ImageService.kt @@ -0,0 +1,35 @@ +package gallery.memories.service + +import android.content.ContentUris +import android.content.Context +import android.graphics.Bitmap +import android.provider.MediaStore +import java.io.ByteArrayOutputStream + +class ImageService(private val mCtx: Context) { + @Throws(Exception::class) + fun getPreview(id: Long): ByteArray { + val bitmap = + MediaStore.Images.Thumbnails.getThumbnail( + mCtx.contentResolver, id, MediaStore.Images.Thumbnails.FULL_SCREEN_KIND, null) + ?: MediaStore.Video.Thumbnails.getThumbnail( + mCtx.contentResolver, id, MediaStore.Video.Thumbnails.FULL_SCREEN_KIND, null) + ?: throw Exception("Thumbnail not found") + + val stream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream) + return stream.toByteArray() + } + + @Throws(Exception::class) + fun getFull(id: Long): ByteArray { + val bitmap = MediaStore.Images.Media.getBitmap( + mCtx.contentResolver, ContentUris.withAppendedId( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)) + ?: throw Exception("Image not found") + + val stream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream) + return stream.toByteArray() + } +} \ No newline at end of file diff --git a/app/src/main/java/gallery/memories/service/TimelineQuery.java b/app/src/main/java/gallery/memories/service/TimelineQuery.java deleted file mode 100644 index b5fc7995..00000000 --- a/app/src/main/java/gallery/memories/service/TimelineQuery.java +++ /dev/null @@ -1,451 +0,0 @@ -package gallery.memories.service; - -import android.app.Activity; -import android.app.PendingIntent; -import android.content.ContentUris; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.icu.text.SimpleDateFormat; -import android.icu.util.TimeZone; -import android.net.Uri; -import android.os.Build; -import android.provider.MediaStore; -import android.text.TextUtils; -import android.util.Log; - -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.IntentSenderRequest; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.appcompat.app.AppCompatActivity; -import androidx.collection.ArraySet; -import androidx.exifinterface.media.ExifInterface; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class TimelineQuery { - final static String TAG = "TimelineQuery"; - AppCompatActivity mCtx; - SQLiteDatabase mDb; - - boolean deleting = false; - ActivityResultLauncher deleteIntentLauncher; - ActivityResult deleteResult; - - public TimelineQuery(AppCompatActivity context) { - mCtx = context; - mDb = new DbService(context).getWritableDatabase(); - - deleteIntentLauncher = mCtx.registerForActivityResult(new ActivityResultContracts.StartIntentSenderForResult(), result -> { - synchronized (deleteIntentLauncher) { - deleteResult = result; - deleteIntentLauncher.notify(); - } - }); - - fullSyncDb(); - } - - public JSONArray getByDayId(final long dayId) throws JSONException { - // Get list of images from DB - final Set imageIds = new ArraySet<>(); - final Map datesTaken = new HashMap<>(); - try (Cursor cursor = mDb.rawQuery( - "SELECT local_id, date_taken FROM images WHERE dayid = ?", - new String[] { Long.toString(dayId) } - )) { - while (cursor.moveToNext()) { - final long localId = cursor.getLong(0); - final long dateTaken = cursor.getLong(1); - imageIds.add(localId); - datesTaken.put(localId, dateTaken); - } - } - - // Nothing to do - if (imageIds.size() == 0) { - return new JSONArray(); - } - - // Filter for given day - String selection = MediaStore.Images.Media._ID - + " IN (" + TextUtils.join(",", imageIds) + ")"; - - // Make list of files - ArrayList files = new ArrayList<>(); - - // Add all images - try (Cursor cursor = mCtx.getContentResolver().query( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - 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, - null, - null - )) { - while (cursor.moveToNext()) { - 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)); - } - } - - // 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 - if (imageIds.size() > 0) { - mDb.execSQL("DELETE FROM images WHERE local_id IN (" + TextUtils.join(",", imageIds) + ")"); - } - - // Return JSON string of files - return new JSONArray(files); - } - - public JSONArray getDays() { - try (Cursor cursor = mDb.rawQuery( - "SELECT dayid, COUNT(local_id) FROM images GROUP BY dayid", - null - )) { - JSONArray days = new JSONArray(); - while (cursor.moveToNext()) { - long id = cursor.getLong(0); - long count = cursor.getLong(1); - days.put(new JSONObject() - .put("dayid", id) - .put("count", count) - ); - } - - return days; - } catch (JSONException e) { - Log.e(TAG, "JSON error"); - return new JSONArray(); - } - } - - public JSONObject getImageInfo(final long id) throws Exception { - try (Cursor cursor = mDb.rawQuery( - "SELECT local_id, date_taken, dayid FROM images WHERE local_id = ?", - new String[] { Long.toString(id) } - )) { - if (!cursor.moveToNext()) { - throw new Exception("Image not found"); - } - - final long localId = cursor.getLong(0); - final long dateTaken = cursor.getLong(1); - final long dayId = cursor.getLong(2); - - try { - return _getImageInfoForCollection( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - localId, dateTaken, dayId - ); - } catch (Exception e) {/* Ignore */} - - try { - 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.DISPLAY_NAME, - MediaStore.Images.Media.MIME_TYPE, - MediaStore.Images.Media.HEIGHT, - MediaStore.Images.Media.WIDTH, - MediaStore.Images.Media.SIZE, - MediaStore.Images.Media.DATA, - }, - selection, - null, - null - )) { - if (!cursor.moveToNext()) { - throw new Exception("Image not found"); - } - - 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; - } - } - - public JSONObject delete(List ids) throws Exception { - synchronized (this) { - if (deleting) { - throw new Exception("Already deleting another set of images"); - } - deleting = true; - } - - try { - // List of URIs - List uris = new ArrayList<>(); - for (long id : ids) { - uris.add(ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)); - } - - // Delete file with media store - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - PendingIntent intent = MediaStore.createTrashRequest(mCtx.getContentResolver(), uris, true); - deleteIntentLauncher.launch(new IntentSenderRequest.Builder(intent.getIntentSender()).build()); - - // Wait for response - synchronized (deleteIntentLauncher) { - deleteIntentLauncher.wait(); - } - - // Throw if canceled or failed - if (deleteResult.getResultCode() != Activity.RESULT_OK) { - throw new Exception("Delete canceled or failed"); - } - } else { - for (Uri uri : uris) { - mCtx.getContentResolver().delete(uri, null, null); - } - } - - // Delete from images table - mDb.execSQL("DELETE FROM images WHERE local_id IN (" + TextUtils.join(",", ids) + ")"); - - return new JSONObject().put("message", "ok"); - } finally { - synchronized (this) { - deleting = false; - } - } - } - - protected void fullSyncDb() { - // Flag all images for removal - mDb.execSQL("UPDATE images SET flag = 1"); - - // Add all images - try (Cursor cursor = mCtx.getContentResolver().query( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - 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 - )) { - while (cursor.moveToNext()) { - insertItemDb( - cursor.getLong(0), - cursor.getString(1), - 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 - 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); - } -} diff --git a/app/src/main/java/gallery/memories/service/TimelineQuery.kt b/app/src/main/java/gallery/memories/service/TimelineQuery.kt new file mode 100644 index 00000000..3ec229dd --- /dev/null +++ b/app/src/main/java/gallery/memories/service/TimelineQuery.kt @@ -0,0 +1,415 @@ +package gallery.memories.service + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.ContentUris +import android.database.sqlite.SQLiteDatabase +import android.icu.text.SimpleDateFormat +import android.icu.util.TimeZone +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.text.TextUtils +import android.util.Log +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.collection.ArraySet +import androidx.exifinterface.media.ExifInterface +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.util.concurrent.CountDownLatch + +class TimelineQuery(private val mCtx: AppCompatActivity) { + private val mDb: SQLiteDatabase = DbService(mCtx).writableDatabase + private val TAG = "TimelineQuery" + + // Photo deletion events + var deleting = false + var deleteIntentLauncher: ActivityResultLauncher + var deleteCallback: ((ActivityResult?) -> Unit)? = null + + init { + // Register intent launcher for callback + deleteIntentLauncher = mCtx.registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result: ActivityResult? -> + synchronized(this) { + deleteCallback?.let { it(result) } + } + } + + // TODO: remove this in favor of a selective sync + fullSyncDb() + } + + @Throws(JSONException::class) + fun getByDayId(dayId: Long): JSONArray { + // Get list of images from DB + val imageIds: MutableSet = ArraySet() + val datesTaken: MutableMap = HashMap() + val sql = "SELECT local_id, date_taken FROM images WHERE dayid = ?" + mDb.rawQuery(sql, arrayOf(dayId.toString())).use { cursor -> + while (cursor.moveToNext()) { + val localId = cursor.getLong(0) + datesTaken[localId] = cursor.getLong(1) + imageIds.add(localId) + } + } + + // Nothing to do + if (imageIds.size == 0) return JSONArray() + + // Filter for given day + val idColName = MediaStore.Images.Media._ID + val imageIdsSl = TextUtils.join(",", imageIds) + val selection = "$idColName IN ($imageIdsSl)" + + // Make list of files + val files = ArrayList() + mCtx.contentResolver.query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + arrayOf( + 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, + null, + null + ).use { cursor -> + while (cursor?.moveToNext() == true) { + val fileId = cursor.getLong(0) + imageIds.remove(fileId) + files.add(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, java.lang.Long.toString(cursor.getLong(6))) + .put(Fields.Photo.DATETAKEN, datesTaken[fileId]) + .put(Fields.Photo.DAYID, dayId)) + } + } + mCtx.contentResolver.query( + MediaStore.Video.Media.EXTERNAL_CONTENT_URI, + arrayOf( + 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 + ).use { cursor -> + while (cursor?.moveToNext() == true) { + // Remove from list of ids + val fileId = cursor.getLong(0) + imageIds.remove(fileId) + files.add(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, java.lang.Long.toString(cursor.getLong(6))) + .put(Fields.Photo.DATETAKEN, datesTaken[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 + if (imageIds.size > 0) { + val delIds = TextUtils.join(",", imageIds) + mDb.execSQL("DELETE FROM images WHERE local_id IN ($delIds)") + } + + // Return JSON string of files + return JSONArray(files) + } + + @Throws(JSONException::class) + fun getDays(): JSONArray { + mDb.rawQuery( + "SELECT dayid, COUNT(local_id) FROM images GROUP BY dayid", + null + ).use { cursor -> + val days = JSONArray() + while (cursor.moveToNext()) { + val id = cursor.getLong(0) + val count = cursor.getLong(1) + days.put(JSONObject() + .put("dayid", id) + .put("count", count) + ) + } + return days + } + } + + @Throws(Exception::class) + fun getImageInfo(id: Long): JSONObject { + val sql = "SELECT local_id, date_taken, dayid FROM images WHERE local_id = ?" + mDb.rawQuery(sql, arrayOf(id.toString())).use { cursor -> + if (!cursor.moveToNext()) { + throw Exception("Image not found") + } + + val localId = cursor.getLong(0) + val dateTaken = cursor.getLong(1) + val dayId = cursor.getLong(2) + + return getImageInfoForCollection( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + localId, dateTaken, dayId) + ?: return getImageInfoForCollection( + MediaStore.Video.Media.EXTERNAL_CONTENT_URI, + localId, dateTaken, dayId) + ?: throw Exception("File not found in any collection") + } + } + + private fun getImageInfoForCollection( + collection: Uri, + localId: Long, + dateTaken: Long, + dayId: Long + ): JSONObject? { + val selection = MediaStore.Images.Media._ID + " = " + localId + mCtx.contentResolver.query( + collection, + arrayOf( + 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.DATA + ), + selection, + null, + null + ).use { cursor -> + if (!cursor!!.moveToNext()) { + throw Exception("Image not found") + } + + // Create basic info + val obj = 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) + val uri = cursor.getString(6) + + // Get EXIF data + try { + val exif = ExifInterface(uri) + obj.put(Fields.Photo.EXIF, JSONObject() + .put("Aperture", exif.getAttribute(ExifInterface.TAG_APERTURE_VALUE)) + .put("FocalLength", exif.getAttribute(ExifInterface.TAG_FOCAL_LENGTH)) + .put("FNumber", exif.getAttribute(ExifInterface.TAG_F_NUMBER)) + .put("ShutterSpeed", exif.getAttribute(ExifInterface.TAG_SHUTTER_SPEED_VALUE)) + .put("ExposureTime", exif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME)) + .put("ISO", exif.getAttribute(ExifInterface.TAG_ISO_SPEED)) + .put("DateTimeOriginal", exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)) + .put("OffsetTimeOriginal", exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL)) + .put("GPSLatitude", exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE)) + .put("GPSLongitude", exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE)) + .put("GPSAltitude", exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE)) + .put("Make", exif.getAttribute(ExifInterface.TAG_MAKE)) + .put("Model", exif.getAttribute(ExifInterface.TAG_MODEL)) + .put("Orientation", exif.getAttribute(ExifInterface.TAG_ORIENTATION)) + .put("Description", exif.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION)) + ) + } catch (e: IOException) { + Log.e(TAG, "Error reading EXIF data for $uri") + } + + return obj + } + } + + @Throws(Exception::class) + fun delete(ids: List): JSONObject { + synchronized(this) { + if (deleting) { + throw Exception("Already deleting another set of images") + } + deleting = true + } + + return try { + // List of URIs + val uris = ids.map { + ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, it) + } + + // Delete file with media store + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val intent = MediaStore.createTrashRequest(mCtx.contentResolver, uris, true) + deleteIntentLauncher.launch(IntentSenderRequest.Builder(intent.intentSender).build()) + + // Wait for response + val latch = CountDownLatch(1) + var res: ActivityResult? = null + deleteCallback = fun(result: ActivityResult?) { + res = result + latch.countDown() + } + latch.await() + deleteCallback = null; + + // Throw if canceled or failed + if (res == null || res!!.resultCode != Activity.RESULT_OK) { + throw Exception("Delete canceled or failed") + } + } else { + for (uri in uris) { + mCtx.contentResolver.delete(uri, null, null) + } + } + + // Delete from images table + val idsList = TextUtils.join(",", ids) + mDb.execSQL("DELETE FROM images WHERE local_id IN ($idsList)") + JSONObject().put("message", "ok") + } finally { + synchronized(this) { deleting = false } + } + } + + protected fun fullSyncDb() { + // Flag all images for removal + mDb.execSQL("UPDATE images SET flag = 1") + mCtx.contentResolver.query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + arrayOf( + 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 + ).use { cursor -> + while (cursor!!.moveToNext()) { + insertItemDb( + cursor.getLong(0), + cursor.getString(1), + cursor.getLong(2), + cursor.getLong(3), + cursor.getString(4), + false + ) + } + } + mCtx.contentResolver.query( + MediaStore.Video.Media.EXTERNAL_CONTENT_URI, + arrayOf( + 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 + ).use { cursor -> + 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") + } + + @SuppressLint("SimpleDateFormat") + private fun insertItemDb( + id: Long, + name: String, + dateTaken: Long, + mtime: Long, + uri: String, + isVideo: Boolean, + ) { + var dateTaken = dateTaken + + // Check if file with local_id and mtime already exists + mDb.rawQuery("SELECT id FROM images WHERE local_id = ?", arrayOf(id.toString())).use { c -> + if (c.count > 0) { + // File already exists, remove flag + mDb.execSQL("UPDATE images SET flag = 0 WHERE local_id = ?", arrayOf(id)) + Log.v(TAG, "File already exists: $id / $name") + return + } + } + + // Get EXIF date using ExifInterface if image + if (!isVideo) { + try { + val exif = ExifInterface(uri!!) + val exifDate = exif.getAttribute(ExifInterface.TAG_DATETIME) + ?: throw IOException() + val sdf = SimpleDateFormat("yyyy:MM:dd HH:mm:ss") + sdf.timeZone = TimeZone.GMT_ZONE + val date = sdf.parse(exifDate) + if (date != null) { + dateTaken = date.time + } + } catch (e: Exception) { + Log.e(TAG, "Failed to read EXIF data: " + e.message) + } + } + + // No way to get the actual local date, so just assume current timezone + if (isVideo) { + dateTaken += TimeZone.getDefault().getOffset(dateTaken).toLong() + } + + // This will use whatever is available + dateTaken /= 1000 + val dayId = dateTaken / 86400 + + // Delete file with same local_id and insert new one + mDb.beginTransaction() + mDb.execSQL("DELETE FROM images WHERE local_id = ?", arrayOf(id)) + mDb.execSQL("INSERT OR IGNORE INTO images (local_id, mtime, basename, date_taken, dayid) VALUES (?, ?, ?, ?, ?)", arrayOf(id, mtime, name, dateTaken, dayId)) + mDb.setTransactionSuccessful() + mDb.endTransaction() + Log.v(TAG, "Inserted file to local DB: $id / $name / $dayId") + } +} \ No newline at end of file