Convert to kotlin
parent
94e47a194c
commit
39f2af8dc3
|
@ -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/");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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/")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, String> 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<Long> parseIds(String ids) {
|
|
||||||
List<Long> result = new ArrayList<>();
|
|
||||||
for (String id : ids.split(",")) {
|
|
||||||
result.add(Long.parseLong(id));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<Long> {
|
||||||
|
return ids.split(",").map { it.toLong() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,6 @@ import gallery.memories.NativeX
|
||||||
|
|
||||||
class DownloadBroadcastReceiver : BroadcastReceiver() {
|
class DownloadBroadcastReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
NativeX.mDlService?.runDownloadCallback(intent)
|
NativeX.getDlService().runDownloadCallback(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,7 +13,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.collection.ArrayMap
|
import androidx.collection.ArrayMap
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
class DownloadService(val mActivity: AppCompatActivity) {
|
class DownloadService(private val mActivity: AppCompatActivity) {
|
||||||
private val mDownloads: MutableMap<Long, () -> Unit> = ArrayMap()
|
private val mDownloads: MutableMap<Long, () -> Unit> = ArrayMap()
|
||||||
|
|
||||||
fun runDownloadCallback(intent: Intent) {
|
fun runDownloadCallback(intent: Intent) {
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<IntentSenderRequest> 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<Long> imageIds = new ArraySet<>();
|
|
||||||
final Map<Long, Long> 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<JSONObject> 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<Long> ids) throws Exception {
|
|
||||||
synchronized (this) {
|
|
||||||
if (deleting) {
|
|
||||||
throw new Exception("Already deleting another set of images");
|
|
||||||
}
|
|
||||||
deleting = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// List of URIs
|
|
||||||
List<Uri> 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<IntentSenderRequest>
|
||||||
|
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<Long?> = ArraySet()
|
||||||
|
val datesTaken: MutableMap<Long, Long> = 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<JSONObject?>()
|
||||||
|
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<Long>): 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")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue