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() {
|
||||
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 java.util.concurrent.CountDownLatch
|
||||
|
||||
class DownloadService(val mActivity: AppCompatActivity) {
|
||||
class DownloadService(private val mActivity: AppCompatActivity) {
|
||||
private val mDownloads: MutableMap<Long, () -> Unit> = ArrayMap()
|
||||
|
||||
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