diff --git a/app/ARDrawingSpace.jks b/app/ARDrawingSpace.jks new file mode 100644 index 0000000..5f28a97 Binary files /dev/null and b/app/ARDrawingSpace.jks differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb43..9e31ffb 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,4 +18,17 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +-keepclassmembers class com.ardrawing.ardrawingspace.MyApplication { + public static final java.lang.String DATABASE_NAME; + public static final int DATABASE_VERSION; +} + +-keepclassmembers class * { + @androidx.room.Query ; +} + +-keep class com.ardrawing.ardrawingspace.room.AppDatabase { *; } +-keep class com.ardrawing.ardrawingspace.room.SpaceEntity { *; } +-keep class com.ardrawing.ardrawingspace.room.SpaceEntityDao { *; } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 87ece83..55a593c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,7 +8,6 @@ - @@ -26,6 +25,15 @@ tools:targetApi="31"> + + + diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/MyApplication.java b/app/src/main/java/com/ardrawing/ardrawingspace/MyApplication.java index 5a9a6ee..3b6b95a 100644 --- a/app/src/main/java/com/ardrawing/ardrawingspace/MyApplication.java +++ b/app/src/main/java/com/ardrawing/ardrawingspace/MyApplication.java @@ -4,6 +4,8 @@ import android.app.Application; import android.content.Context; import android.content.SharedPreferences; +import com.ardrawing.ardrawingspace.util.FileParsingAndInitialization; + public class MyApplication extends Application { public static MyApplication application; public static final String DATABASE_NAME = "database"; @@ -27,8 +29,8 @@ public class MyApplication extends Application { } private void initDatabase() { -// InitDatabase initDatabase = new InitDatabase(getContext()); -// initDatabase.insertImagesToDatabase(); + FileParsingAndInitialization fileParsingAndInitialization = new FileParsingAndInitialization(application); + fileParsingAndInitialization.insertImagesToDatabase(); } public static Context getContext() { diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/activity/CategoryActivity.java b/app/src/main/java/com/ardrawing/ardrawingspace/activity/CategoryActivity.java new file mode 100644 index 0000000..4d5c605 --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/activity/CategoryActivity.java @@ -0,0 +1,84 @@ +package com.ardrawing.ardrawingspace.activity; + +import android.os.Bundle; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.GridLayoutManager; + +import com.ardrawing.ardrawingspace.R; +import com.ardrawing.ardrawingspace.adapter.SpaceRecyclerViewAdapter; +import com.ardrawing.ardrawingspace.databinding.ActivityCategoryBinding; +import com.ardrawing.ardrawingspace.room.SpaceEntity; +import com.ardrawing.ardrawingspace.util.ItemDecoration; +import com.ardrawing.ardrawingspace.viewmodel.SpaceViewModel; + +import java.util.ArrayList; +import java.util.List; + +public class CategoryActivity extends AppCompatActivity { + private ActivityCategoryBinding binding; + private SpaceRecyclerViewAdapter adapter; + private SpaceViewModel viewModel; + private String title; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + binding = ActivityCategoryBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + + initData(); + initEvent(); + } + + private void initData() { + String path = getIntent().getStringExtra("imagePath"); + if (path == null) { + finish(); + return; + } + + String[] parts = path.split("/"); + title = parts[0].split("_")[1]; + + viewModel = new ViewModelProvider(this).get(SpaceViewModel.class); + + binding.recyclerView.setLayoutManager(new GridLayoutManager(this, 2)); + adapter = new SpaceRecyclerViewAdapter(this, new ArrayList<>(), 2); + binding.recyclerView.setAdapter(adapter); + + binding.recyclerView.addItemDecoration(new ItemDecoration(35, 15, 20)); + } + + private void initEvent() { + binding.back.setOnClickListener(v -> finish()); + binding.text.setText(title); + loadImage(); + } + + private void loadImage() { + viewModel + .getEntityByImagePath(title) + .observe(this, spaceEntityList -> { + adapter.updateItems(spaceEntityList); + }); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + binding = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/activity/DrawingActivity.java b/app/src/main/java/com/ardrawing/ardrawingspace/activity/DrawingActivity.java new file mode 100644 index 0000000..85d289e --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/activity/DrawingActivity.java @@ -0,0 +1,284 @@ +package com.ardrawing.ardrawingspace.activity; + +import android.Manifest; +import android.content.ContentValues; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.provider.MediaStore; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageView; +import android.widget.SeekBar; +import android.widget.Toast; + +import androidx.activity.EdgeToEdge; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.camera.core.Camera; +import androidx.camera.core.CameraSelector; +import androidx.camera.core.ImageCapture; +import androidx.camera.core.ImageCaptureException; +import androidx.camera.core.Preview; +import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.ardrawing.ardrawingspace.R; +import com.ardrawing.ardrawingspace.databinding.ActivityDrawingBinding; +import com.ardrawing.ardrawingspace.util.DocumentManipulationTool; +import com.ardrawing.ardrawingspace.util.PermissionTool; +import com.google.common.util.concurrent.ListenableFuture; + +import org.jetbrains.annotations.Nullable; + +public class DrawingActivity extends AppCompatActivity implements SeekBar.OnSeekBarChangeListener, View.OnTouchListener { + private ActivityDrawingBinding binding; + + private static final int CAMERA_PERMISSION_REQUEST = 200; + private static final int STORAGE_PERMISSION_REQUEST = 201; + private static final int PICK_IMAGE_REQUEST = 202; + + private static final int MODE_NONE = 0; + private static final int MODE_DRAG = 1; + private static final int MODE_ZOOM = 2; + + private final Matrix matrix = new Matrix(); + private final Matrix savedMatrix = new Matrix(); + private final PointF startPoint = new PointF(); + private float initialDistance; + private int mode = MODE_NONE; + + private boolean isFlash = false; + private Camera camera; + private ImageCapture imageCapture; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityDrawingBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + EdgeToEdge.enable(this); + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + + setupEventListeners(); + } + + private void setupEventListeners() { + String imagePath = getIntent().getStringExtra("imagePath"); + if (imagePath == null) { + finish(); + return; + } + + DocumentManipulationTool.loadImageByPath(imagePath, binding.imageView, this); + + binding.imageView.setOnTouchListener(this); + binding.seekbar.setOnSeekBarChangeListener(this); + binding.photo.setOnClickListener(v -> handleImagePicker()); + binding.flash.setOnClickListener(v -> toggleFlash()); + binding.back.setOnClickListener(v -> finish()); + binding.camera.setOnClickListener(v -> captureAndSaveImage()); + + checkAndRequestPermissions(); + } + + private void checkAndRequestPermissions() { + String[] permissions = getPermissions(); + if (!PermissionTool.hasPermission(this, permissions)) { + PermissionTool.reqPermission(this, permissions, CAMERA_PERMISSION_REQUEST); + } else { + setupCamera(); + } + } + + private void setupCamera() { + ListenableFuture future = ProcessCameraProvider.getInstance(this); + future.addListener(() -> { + try { + ProcessCameraProvider provider = future.get(); + Preview preview = new Preview.Builder().build(); + preview.setSurfaceProvider(binding.preview.getSurfaceProvider()); + + imageCapture = new ImageCapture.Builder().build(); + CameraSelector selector = new CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build(); + + provider.unbindAll(); + camera = provider.bindToLifecycle(this, selector, preview, imageCapture); + } catch (Exception ignored) { + } + }, ContextCompat.getMainExecutor(this)); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + handlePermissionResult(requestCode, grantResults); + } + + private void handlePermissionResult(int requestCode, int[] grantResults) { + if (requestCode == CAMERA_PERMISSION_REQUEST && PermissionTool.gotPermission(grantResults)) { + setupCamera(); + } else if (requestCode == STORAGE_PERMISSION_REQUEST && PermissionTool.gotPermission(grantResults)) { + handleImagePicker(); + } else { + showToast("Permission denied. Please enable it in Settings."); + } + } + + private void captureAndSaveImage() { + if (imageCapture == null) { + showToast("Camera not Init"); + return; + } + + if (!hasCameraPermission()) { + requestCameraPermission(); + return; + } + + ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder( + getContentResolver(), + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + prepareContentValues() + ).build(); + + imageCapture.takePicture(options, ContextCompat.getMainExecutor(this), new ImageCapture.OnImageSavedCallback() { + @Override + public void onImageSaved(@NonNull ImageCapture.OutputFileResults output) { + showToast("Successful photo shoot"); + } + + @Override + public void onError(@NonNull ImageCaptureException exception) { + showToast("Photo shoot failed"); + } + }); + } + + private void handleImagePicker() { + String[] permissions = PermissionTool.getPermission(); + if (ContextCompat.checkSelfPermission(this, permissions[0]) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, permissions, STORAGE_PERMISSION_REQUEST); + } else { + Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + startActivityForResult(intent, PICK_IMAGE_REQUEST); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null) { + Uri imageUri = data.getData(); + if (imageUri != null) { + binding.imageView.setImageURI(imageUri); + } + } + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + binding.imageView.setAlpha((100 - progress) / 100f); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + ImageView view = (ImageView) v; + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + savedMatrix.set(matrix); + startPoint.set(event.getX(), event.getY()); + mode = MODE_DRAG; + break; + case MotionEvent.ACTION_POINTER_DOWN: + initialDistance = DocumentManipulationTool.computeTouchDistance(event); + if (initialDistance > 10f) { + savedMatrix.set(matrix); + mode = MODE_ZOOM; + } + break; + case MotionEvent.ACTION_MOVE: + if (mode == MODE_DRAG) { + matrix.set(savedMatrix); + matrix.postTranslate(event.getX() - startPoint.x, event.getY() - startPoint.y); + } else if (mode == MODE_ZOOM && event.getPointerCount() >= 2) { + float newDist = DocumentManipulationTool.computeTouchDistance(event); + if (newDist > 10f) { + float scale = newDist / initialDistance; + matrix.set(savedMatrix); + matrix.postScale(scale, scale, view.getWidth() / 2f, view.getHeight() / 2f); + } + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + mode = MODE_NONE; + break; + } + view.setImageMatrix(matrix); + return true; + } + + private void toggleFlash() { + if (camera != null) { + isFlash = !isFlash; + camera.getCameraControl().enableTorch(isFlash); + binding.flash.setImageResource(isFlash ? R.drawable.flash : R.drawable.un_flash); + } + } + + private boolean hasCameraPermission() { + return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED; + } + + private void requestCameraPermission() { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, CAMERA_PERMISSION_REQUEST); + } + + private ContentValues prepareContentValues() { + ContentValues values = new ContentValues(); + values.put(MediaStore.MediaColumns.DISPLAY_NAME, "photo_" + System.currentTimeMillis() + ".jpg"); + values.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg"); + values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES); + return values; + } + + private String[] getPermissions() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ? + new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_MEDIA_IMAGES} : + new String[]{Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}; + } + + private void showToast(String message) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + binding = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/activity/MainActivity.java b/app/src/main/java/com/ardrawing/ardrawingspace/activity/MainActivity.java index 906d02b..6a85e54 100644 --- a/app/src/main/java/com/ardrawing/ardrawingspace/activity/MainActivity.java +++ b/app/src/main/java/com/ardrawing/ardrawingspace/activity/MainActivity.java @@ -1,26 +1,166 @@ package com.ardrawing.ardrawingspace.activity; import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; import androidx.activity.EdgeToEdge; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.ardrawing.ardrawingspace.R; +import com.ardrawing.ardrawingspace.adapter.ViewPagerAdapter; +import com.ardrawing.ardrawingspace.databinding.ActivityMainBinding; +import com.ardrawing.ardrawingspace.databinding.TabCustomBinding; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; public class MainActivity extends AppCompatActivity { + private ActivityMainBinding viewBinding; + + private static final int[] ACTIVE_ICONS = { + R.drawable.home, R.drawable.resource_import, R.drawable.collection + }; + + private static final int[] INACTIVE_ICONS = { + R.drawable.un_home, R.drawable.un_import, R.drawable.un_collection + }; + + private static final int COLOR_BLACK = R.color.black; + private static final int COLOR_GRAY = R.color.gray; + + private static final TabConfig[] TAB_CONFIGS = { + new TabConfig(R.drawable.home, "Category", COLOR_BLACK), + new TabConfig(R.drawable.un_import, "Import", COLOR_GRAY), + new TabConfig(R.drawable.un_collection, "Favorite", COLOR_GRAY) + }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + configureEdgeToEdge(); + initializeBinding(); + setupWindowInsets(); + + initData(); + initEvent(); + } + + private void configureEdgeToEdge() { EdgeToEdge.enable(this); - setContentView(R.layout.activity_main); - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { - Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); - return insets; + } + + private void initializeBinding() { + viewBinding = ActivityMainBinding.inflate(LayoutInflater.from(this)); + setContentView(viewBinding.getRoot()); + } + + private void setupWindowInsets() { + View mainLayout = findViewById(R.id.main); + ViewCompat.setOnApplyWindowInsetsListener(mainLayout, (view, windowInsets) -> { + Insets bars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); + view.setPadding(bars.left, bars.top, bars.right, bars.bottom); + return windowInsets; }); } + + private void initData() { + ViewPagerAdapter pagerAdapter = new ViewPagerAdapter(this); + viewBinding.viewPager.setAdapter(pagerAdapter); + } + + private void initEvent() { + TabLayoutMediator mediator = new TabLayoutMediator( + viewBinding.tabLayout, + viewBinding.viewPager, + this::configureTabView + ); + mediator.attach(); + + viewBinding.tabLayout.addOnTabSelectedListener(createTabListener()); + } + + private void configureTabView(TabLayout.Tab tab, int index) { + TabCustomBinding tabViewBinding = TabCustomBinding.inflate(LayoutInflater.from(this)); + tab.setCustomView(tabViewBinding.getRoot()); + TabConfig config = determineTabConfig(index); + tabViewBinding.imageView.setImageResource(config.iconId); + tabViewBinding.title.setText(config.label); + tabViewBinding.title.setTextColor(fetchColor(config.textColorId)); + } + + private TabLayout.OnTabSelectedListener createTabListener() { + return new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + refreshTabAppearance(tab, true); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + refreshTabAppearance(tab, false); + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + } + + private void refreshTabAppearance(TabLayout.Tab tab, boolean active) { + View customView = tab.getCustomView(); + if (customView != null) { + TabCustomBinding tabBinding = TabCustomBinding.bind(customView); + updateTabIcon(tabBinding, tab.getPosition(), active); + updateTabTextColor(tabBinding, active); + } + } + + private void updateTabIcon(TabCustomBinding tabBinding, int pos, boolean active) { + int iconId = fetchIconResource(pos, active); + tabBinding.imageView.setImageResource(iconId); + } + + private void updateTabTextColor(TabCustomBinding tabBinding, boolean active) { + int colorId = active ? COLOR_BLACK : COLOR_GRAY; + tabBinding.title.setTextColor(fetchColor(colorId)); + } + + private int fetchIconResource(int pos, boolean active) { + return active ? ACTIVE_ICONS[pos] : INACTIVE_ICONS[pos]; + } + }; + } + + private TabConfig determineTabConfig(int index) { + if (index >= 0 && index < TAB_CONFIGS.length) { + return TAB_CONFIGS[index]; + } + throw new IllegalStateException("Unexpected tab index: " + index); + } + + private static class TabConfig { + final int iconId; + final String label; + final int textColorId; + + TabConfig(int iconId, String label, int textColorId) { + this.iconId = iconId; + this.label = label; + this.textColorId = textColorId; + } + } + + private int fetchColor(int colorRes) { + return ContextCompat.getColor(this, colorRes); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (viewBinding != null) { + viewBinding = null; + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/activity/SplashActivity.java b/app/src/main/java/com/ardrawing/ardrawingspace/activity/SplashActivity.java new file mode 100644 index 0000000..0e7792d --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/activity/SplashActivity.java @@ -0,0 +1,78 @@ +package com.ardrawing.ardrawingspace.activity; + +import android.content.Intent; +import android.os.Bundle; +import android.os.CountDownTimer; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.ardrawing.ardrawingspace.R; +import com.ardrawing.ardrawingspace.databinding.ActivitySplashBinding; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; + +import kotlin.Unit; +import kotlin.jvm.functions.Function0; +import kotlin.jvm.functions.Function1; + +public class SplashActivity extends AppCompatActivity { + private ActivitySplashBinding binding; + private static final long TOTAL_TIME = 1000; + private CountDownTimer countDownTimer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + + binding = ActivitySplashBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + + Glide.with(this) + .load(R.mipmap.ic_launcher) + .transform(new RoundedCorners(16)) + .into(binding.splashImage); + + countDownTimer = new CountDownTimer(TOTAL_TIME, 100) { + @Override + public void onTick(long millisUntilFinished) { + int percentage = (int) (100 - (float) millisUntilFinished / TOTAL_TIME * 100); + binding.progressBar.setProgress(percentage); + } + + @Override + public void onFinish() { + startMain(); + } + }; + + countDownTimer.start(); + } + + private void startMain() { + binding.progressBar.setProgress(100); + + Intent intent = new Intent(SplashActivity.this, MainActivity.class); + startActivity(intent); + finish(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (countDownTimer != null) { + countDownTimer.cancel(); + } + binding = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/adapter/SpaceRecyclerViewAdapter.java b/app/src/main/java/com/ardrawing/ardrawingspace/adapter/SpaceRecyclerViewAdapter.java new file mode 100644 index 0000000..8dbb14b --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/adapter/SpaceRecyclerViewAdapter.java @@ -0,0 +1,205 @@ +package com.ardrawing.ardrawingspace.adapter; + +import android.content.Context; +import android.content.Intent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +import com.ardrawing.ardrawingspace.R; +import com.ardrawing.ardrawingspace.activity.CategoryActivity; +import com.ardrawing.ardrawingspace.activity.DrawingActivity; +import com.ardrawing.ardrawingspace.room.AppDatabase; +import com.ardrawing.ardrawingspace.room.SpaceEntity; +import com.ardrawing.ardrawingspace.util.AppExecutors; +import com.ardrawing.ardrawingspace.util.DeleteCallback; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; + +import java.util.ArrayList; +import java.util.List; + +public class SpaceRecyclerViewAdapter extends RecyclerView.Adapter { + private static final int DISPLAY_MODE_CATEGORY = 0; + private static final int DISPLAY_MODE_DRAWING = 1; + private static final int CORNER_RADIUS = 32; + private static final int PLACEHOLDER_RES = R.mipmap.placeholder; + private static final int LIKE_ICON = R.drawable.like; + private static final int UNLIKE_ICON = R.drawable.un_like; + + private List spaceEntityList; + private final Context context; + private final int displayMode; + private DeleteCallback deleteCallback; + + public SpaceRecyclerViewAdapter(Context context, List spaceEntityList, int displayMode) { + this.context = context; + this.spaceEntityList = new ArrayList<>(spaceEntityList); + this.displayMode = displayMode; + } + + public void updateItems(List newItems) { + DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new SpaceDiffCallback(this.spaceEntityList, newItems)); + this.spaceEntityList = new ArrayList<>(newItems); + diffResult.dispatchUpdatesTo(this); + } + + public void setDeleteCallback(DeleteCallback deleteCallback) { + this.deleteCallback = deleteCallback; + } + + @NonNull + @Override + public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(context); + View itemView = inflater.inflate(R.layout.item_space, parent, false); + return new ItemViewHolder(itemView); + } + + @Override + public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { + SpaceEntity currentItem = spaceEntityList.get(position); + String displayName = getTitle(currentItem); + holder.renderItem(currentItem, displayName, displayMode); + } + + private String getTitle(SpaceEntity item) { + if (displayMode == DISPLAY_MODE_CATEGORY) { + String path = item.getImagePath(); + String[] segments = path.split("/"); + return segments.length > 0 ? segments[0].split("_")[1] : ""; + } + return ""; + } + + @Override + public int getItemCount() { + return spaceEntityList.size(); + } + + public class ItemViewHolder extends RecyclerView.ViewHolder { + private final ImageView previewImage; + private final ImageView likeIcon; + private final ImageView deleteIcon; + private final TextView titleText; + + public ItemViewHolder(@NonNull View view) { + super(view); + previewImage = view.findViewById(R.id.item_image); + likeIcon = view.findViewById(R.id.like); + deleteIcon = view.findViewById(R.id.delete); + titleText = view.findViewById(R.id.title); + } + + void renderItem(SpaceEntity item, String displayName, int mode) { + if (mode == DISPLAY_MODE_CATEGORY) { + titleText.setText(displayName); + titleText.setVisibility(View.VISIBLE); + likeIcon.setVisibility(View.GONE); + deleteIcon.setVisibility(View.GONE); + } else if (mode == DISPLAY_MODE_DRAWING) { + titleText.setVisibility(View.GONE); + likeIcon.setVisibility(View.VISIBLE); + deleteIcon.setVisibility(View.VISIBLE); + } + + loadImageContent(item.getImagePath()); + updateFavoriteIcon(item); + attachListeners(item, mode); + } + + private void loadImageContent(String path) { + String formattedPath = adjustImagePath(path); + Glide.with(context) + .load(formattedPath) + .placeholder(PLACEHOLDER_RES) + .apply(new RequestOptions() + .transform(new CenterCrop(), new RoundedCorners(CORNER_RADIUS))) + .into(previewImage); + } + + private String adjustImagePath(String path) { + return path.startsWith("/data/user/") ? path : "file:///android_asset/" + path; + } + + private void updateFavoriteIcon(SpaceEntity item) { + int iconRes = item.isFavorite() ? LIKE_ICON : UNLIKE_ICON; + likeIcon.setImageResource(iconRes); + } + + private void attachListeners(SpaceEntity item, int mode) { + previewImage.setOnClickListener(v -> handleImageTap(item, mode)); + likeIcon.setOnClickListener(v -> toggleFavorite(item)); + deleteIcon.setOnClickListener(v -> onDeleteClicked(item)); + } + + private void onDeleteClicked(SpaceEntity item) { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION && deleteCallback != null) { + deleteCallback.onDelete(item); + } + } + + private void handleImageTap(SpaceEntity item, int mode) { + Intent intent = createIntent(item, mode); + context.startActivity(intent); + } + + private Intent createIntent(SpaceEntity item, int mode) { + Class targetActivity = mode == DISPLAY_MODE_CATEGORY ? CategoryActivity.class : DrawingActivity.class; + return new Intent(context, targetActivity) + .putExtra("imagePath", item.getImagePath()); + } + + private void toggleFavorite(SpaceEntity item) { + item.setFavorite(!item.isFavorite()); + persistItemUpdate(item); + notifyItemChanged(getAdapterPosition()); + } + + private void persistItemUpdate(SpaceEntity item) { + AppExecutors.getInstance().diskIO().execute(() -> { + AppDatabase db = AppDatabase.getInstance(context); + db.dao().update(item); + }); + } + } + + private static class SpaceDiffCallback extends DiffUtil.Callback { + private final List oldList; + private final List newList; + + public SpaceDiffCallback(List oldList, List newList) { + this.oldList = oldList != null ? new ArrayList<>(oldList) : new ArrayList<>(); + this.newList = newList != null ? new ArrayList<>(newList) : new ArrayList<>(); + } + + @Override + public int getOldListSize() { + return oldList.size(); + } + + @Override + public int getNewListSize() { + return newList.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return oldList.get(oldItemPosition).getId() == newList.get(newItemPosition).getId(); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return oldList.get(oldItemPosition).equals(newList.get(newItemPosition)); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/adapter/ViewPagerAdapter.java b/app/src/main/java/com/ardrawing/ardrawingspace/adapter/ViewPagerAdapter.java new file mode 100644 index 0000000..6e45ad4 --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/adapter/ViewPagerAdapter.java @@ -0,0 +1,36 @@ +package com.ardrawing.ardrawingspace.adapter; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +import com.ardrawing.ardrawingspace.fragment.FavoriteFragment; +import com.ardrawing.ardrawingspace.fragment.HomeFragment; +import com.ardrawing.ardrawingspace.fragment.ImportFragment; + +import java.util.ArrayList; +import java.util.List; + +public class ViewPagerAdapter extends FragmentStateAdapter { + private final List fragmentList; + + public ViewPagerAdapter(@NonNull FragmentActivity fragmentActivity) { + super(fragmentActivity); + fragmentList = new ArrayList<>(); + fragmentList.add(new HomeFragment()); + fragmentList.add(new ImportFragment()); + fragmentList.add(new FavoriteFragment()); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + return fragmentList.get(position); + } + + @Override + public int getItemCount() { + return fragmentList.size(); + } +} diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/fragment/FavoriteFragment.java b/app/src/main/java/com/ardrawing/ardrawingspace/fragment/FavoriteFragment.java new file mode 100644 index 0000000..e8a0031 --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/fragment/FavoriteFragment.java @@ -0,0 +1,76 @@ +package com.ardrawing.ardrawingspace.fragment; + +import android.os.Bundle; + +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.GridLayoutManager; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.ardrawing.ardrawingspace.R; +import com.ardrawing.ardrawingspace.adapter.SpaceRecyclerViewAdapter; +import com.ardrawing.ardrawingspace.databinding.FragmentFavoriteBinding; +import com.ardrawing.ardrawingspace.room.SpaceEntity; +import com.ardrawing.ardrawingspace.util.ItemDecoration; +import com.ardrawing.ardrawingspace.viewmodel.SpaceViewModel; + +import java.util.ArrayList; +import java.util.List; + +public class FavoriteFragment extends Fragment { + private FragmentFavoriteBinding binding; + private SpaceViewModel viewModel; + private SpaceRecyclerViewAdapter adapter; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + binding = FragmentFavoriteBinding.inflate(inflater, container, false); + + initData(); + initEvent(); + + return binding.getRoot(); + } + + private void initData() { + viewModel = new ViewModelProvider(this).get(SpaceViewModel.class); + + binding.recyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); + + adapter = new SpaceRecyclerViewAdapter(requireContext(), new ArrayList<>(), 2); + binding.recyclerView.setAdapter(adapter); + + binding.recyclerView.addItemDecoration(new ItemDecoration(35, 15, 20)); + } + + private void initEvent() { + loadLike(); + } + + private void loadLike() { + viewModel + .getFavorite() + .observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(List spaceEntityList) { + if (spaceEntityList.isEmpty()) { + binding.text.setVisibility(View.VISIBLE); + } else { + binding.text.setVisibility(View.GONE); + } + adapter.updateItems(spaceEntityList); + } + }); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/fragment/HomeFragment.java b/app/src/main/java/com/ardrawing/ardrawingspace/fragment/HomeFragment.java new file mode 100644 index 0000000..9a97741 --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/fragment/HomeFragment.java @@ -0,0 +1,77 @@ +package com.ardrawing.ardrawingspace.fragment; + +import android.os.Bundle; + +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.GridLayoutManager; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.ardrawing.ardrawingspace.R; +import com.ardrawing.ardrawingspace.adapter.SpaceRecyclerViewAdapter; +import com.ardrawing.ardrawingspace.databinding.FragmentFavoriteBinding; +import com.ardrawing.ardrawingspace.databinding.FragmentHomeBinding; +import com.ardrawing.ardrawingspace.room.SpaceEntity; +import com.ardrawing.ardrawingspace.util.ItemDecoration; +import com.ardrawing.ardrawingspace.viewmodel.SpaceViewModel; + +import java.util.ArrayList; +import java.util.List; + +public class HomeFragment extends Fragment { + private FragmentHomeBinding binding; + private SpaceViewModel viewModel; + private SpaceRecyclerViewAdapter adapter; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + binding = FragmentHomeBinding.inflate(inflater, container, false); + + initData(); + initEvent(); + + return binding.getRoot(); + } + + private void initData() { + viewModel = new ViewModelProvider(this).get(SpaceViewModel.class); + + binding.recyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); + + adapter = new SpaceRecyclerViewAdapter(requireContext(), new ArrayList<>(), 0); + binding.recyclerView.setAdapter(adapter); + + binding.recyclerView.addItemDecoration(new ItemDecoration(35, 15, 20)); + } + + private void initEvent() { + loadLike(); + } + + private void loadLike() { + viewModel + .getMinById() + .observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(List spaceEntityList) { + if (spaceEntityList.isEmpty()) { + binding.text.setVisibility(View.VISIBLE); + } else { + binding.text.setVisibility(View.GONE); + } + adapter.updateItems(spaceEntityList); + } + }); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/fragment/ImportFragment.java b/app/src/main/java/com/ardrawing/ardrawingspace/fragment/ImportFragment.java new file mode 100644 index 0000000..e558cee --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/fragment/ImportFragment.java @@ -0,0 +1,118 @@ +package com.ardrawing.ardrawingspace.fragment; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; + +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.GridLayoutManager; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import com.ardrawing.ardrawingspace.adapter.SpaceRecyclerViewAdapter; +import com.ardrawing.ardrawingspace.databinding.FragmentImportBinding; +import com.ardrawing.ardrawingspace.room.SpaceEntity; +import com.ardrawing.ardrawingspace.util.DeleteCallback; +import com.ardrawing.ardrawingspace.util.ItemDecoration; +import com.ardrawing.ardrawingspace.util.UploadUtil; +import com.ardrawing.ardrawingspace.viewmodel.SpaceViewModel; + +import java.util.ArrayList; +import java.util.List; + +public class ImportFragment extends Fragment implements DeleteCallback { + private static final int PICK_IMAGE_REQUEST_CODE = 202; + private FragmentImportBinding binding; + private SpaceRecyclerViewAdapter adapter; + private SpaceViewModel viewModel; + private final List imagePaths = new ArrayList<>(); + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + binding = FragmentImportBinding.inflate(inflater, container, false); + + initData(); + initEvent(); + + return binding.getRoot(); + } + + private void initData() { + viewModel = new ViewModelProvider(this).get(SpaceViewModel.class); + + binding.recyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); + + adapter = new SpaceRecyclerViewAdapter(requireContext(), new ArrayList<>(), 1); + binding.recyclerView.setAdapter(adapter); + adapter.setDeleteCallback(this); + + binding.recyclerView.addItemDecoration(new ItemDecoration(35, 15, 20)); + } + + private void initEvent() { + binding.upload.setOnClickListener(v -> { + openImagePicker(); + }); + + loadAllImportImage(); + } + + private void loadAllImportImage() { + viewModel + .getImportLiveData() + .observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(List spaceEntityList) { + if (spaceEntityList.isEmpty()) { + binding.text.setVisibility(View.VISIBLE); + } else { + binding.text.setVisibility(View.GONE); + } + adapter.updateItems(spaceEntityList); + } + }); + } + + private void openImagePicker() { + Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + startActivityForResult(intent, PICK_IMAGE_REQUEST_CODE); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == PICK_IMAGE_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) { + Uri selectedImageUri = data.getData(); + if (selectedImageUri != null) { + processImage(selectedImageUri); + } + } + } + + private void processImage(Uri imageUri) { + if (UploadUtil.isImageSizeValid(imageUri, requireContext())) { + UploadUtil.saveImageByPath(imageUri, imagePaths, viewModel, requireContext()); + } else { + Toast.makeText(getContext(), "The image size is out of limit", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + binding = null; + } + + @Override + public void onDelete(SpaceEntity entity) { + UploadUtil.deleteImageByPath(entity, viewModel, requireContext()); + loadAllImportImage(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/util/AppExecutors.java b/app/src/main/java/com/ardrawing/ardrawingspace/util/AppExecutors.java new file mode 100644 index 0000000..152becb --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/util/AppExecutors.java @@ -0,0 +1,58 @@ +package com.ardrawing.ardrawingspace.util; + +import android.os.Handler; +import android.os.Looper; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class AppExecutors { + private static AppExecutors instance; + + // 磁盘 I/O 线程池(单线程,用于数据库操作) + private final Executor diskIO; + + // 主线程执行器(用于 UI 更新) + private final Executor mainThread; + + // 网络线程池(可根据需要调整线程数) + private final Executor networkIO; + + private AppExecutors() { + this.diskIO = Executors.newSingleThreadExecutor(); + this.mainThread = new MainThreadExecutor(); + this.networkIO = Executors.newFixedThreadPool(3); // 3 个线程处理网络任务 + } + + public static AppExecutors getInstance() { + if (instance == null) { + synchronized (AppExecutors.class) { + if (instance == null) { + instance = new AppExecutors(); + } + } + } + return instance; + } + + public Executor diskIO() { + return diskIO; + } + + public Executor mainThread() { + return mainThread; + } + + public Executor networkIO() { + return networkIO; + } + + private static class MainThreadExecutor implements Executor { + private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(Runnable command) { + mainThreadHandler.post(command); + } + } +} diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/util/DeleteCallback.java b/app/src/main/java/com/ardrawing/ardrawingspace/util/DeleteCallback.java new file mode 100644 index 0000000..19d0a1c --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/util/DeleteCallback.java @@ -0,0 +1,7 @@ +package com.ardrawing.ardrawingspace.util; + +import com.ardrawing.ardrawingspace.room.SpaceEntity; + +public interface DeleteCallback { + void onDelete(SpaceEntity entity); +} diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/util/DocumentManipulationTool.java b/app/src/main/java/com/ardrawing/ardrawingspace/util/DocumentManipulationTool.java new file mode 100644 index 0000000..cb00407 --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/util/DocumentManipulationTool.java @@ -0,0 +1,78 @@ +package com.ardrawing.ardrawingspace.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; +import android.view.MotionEvent; +import android.widget.ImageView; +import android.widget.Toast; + +import java.io.IOException; +import java.io.InputStream; + +public class DocumentManipulationTool { + private static final boolean IS_DEBUG = false; + + public static float computeTouchDistance(MotionEvent event) { + if (event == null || event.getPointerCount() < 2) return 0f; + + float deltaX = event.getX(1) - event.getX(0); + float deltaY = event.getY(1) - event.getY(0); + return (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY); + } + + public static void loadImageByPath(String path, ImageView imageView, Context context) { + if (isInvalidInput(path, imageView, context)) return; + + if (path.startsWith("/data/user/")) { + loadCheck(path, imageView, context); + } else { + loadTry(path, imageView, context); + } + } + + private static boolean isInvalidInput(String path, ImageView imageView, Context context) { + return path == null || imageView == null || context == null; + } + + private static void loadCheck(String path, ImageView imageView, Context context) { + Bitmap bitmap = decodeBitmapFromFile(path); + if (bitmap != null) { + imageView.setImageBitmap(bitmap); + } else { + displayError(context, "Failed to load image from storage"); + } + } + + private static void loadTry(String path, ImageView imageView, Context context) { + try (InputStream inputStream = context.getAssets().open(path)) { + Bitmap bitmap = BitmapFactory.decodeStream(inputStream); + if (bitmap != null) { + imageView.setImageBitmap(bitmap); + } else { + displayError(context, "Failed to decode asset image"); + } + } catch (IOException e) { + displayError(context, "Failed to load image from assets"); + } + } + + private static Bitmap decodeBitmapFromFile(String path) { + if (path.isEmpty()) return null; + return BitmapFactory.decodeFile(path); + } + + private static void displayError(Context context, String message) { + if (context != null) { + showToast(context, message); + } + } + + private static void showToast(Context context, String message) { + if (IS_DEBUG) { + Log.d("FileUtil", message); + } + Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/util/FileParsingAndInitialization.java b/app/src/main/java/com/ardrawing/ardrawingspace/util/FileParsingAndInitialization.java new file mode 100644 index 0000000..1d354b0 --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/util/FileParsingAndInitialization.java @@ -0,0 +1,71 @@ +package com.ardrawing.ardrawingspace.util; + +import android.app.Application; +import android.content.res.AssetManager; +import android.util.Log; + +import com.ardrawing.ardrawingspace.room.SpaceEntity; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class FileParsingAndInitialization { + private final Application application; + private static final String IMAGE_PREFIX = "png_"; + private static final String IMAGE_EXTENSION = ".jpg"; + + public FileParsingAndInitialization(Application application) { + this.application = application; + } + + public void insertImagesToDatabase() { + AssetManager assetManager = application.getAssets(); + try { + String[] directories = assetManager.list(""); + if (directories == null) { + Log.w("InitDatabase", "No directories found in assets"); + return; + } + + for (String dir : directories) { + if (!dir.startsWith(IMAGE_PREFIX)) continue; + importImagesFromDirectory(dir); + } + } catch (IOException e) { + Log.e("InitDatabase", "Failed to list asset directories", e); + } + } + + private void importImagesFromDirectory(String directory) { + AssetManager assetManager = application.getAssets(); + try { + String[] files = assetManager.list(directory); + if (files == null) { + Log.w("InitDatabase", "No files found in directory: " + directory); + return; + } + + List imageList = new ArrayList<>(); + for (String file : files) { + if (file.endsWith(IMAGE_EXTENSION)) { + String imagePath = directory + "/" + file; + imageList.add(new SpaceEntity(imagePath, false, false)); + } + } + + if (!imageList.isEmpty()) { + insertImagesAsync(imageList); + } + } catch (IOException e) { + Log.e("InitDatabase", "Failed to list files in directory: " + directory, e); + } + } + + private void insertImagesAsync(List imageList) { + new Thread(() -> { + SpaceRepository repository = new SpaceRepository(application); + repository.insertAll(imageList); + }).start(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/util/PermissionTool.java b/app/src/main/java/com/ardrawing/ardrawingspace/util/PermissionTool.java new file mode 100644 index 0000000..bbdf678 --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/util/PermissionTool.java @@ -0,0 +1,63 @@ +package com.ardrawing.ardrawingspace.util; + +import android.Manifest; +import android.app.Activity; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +public final class PermissionTool { + private PermissionTool() { + // 防止实例化 + } + + private static final boolean IS_DEBUG = false; + + public static boolean hasPermission(Activity activity, String[] permissions) { + if (isInvalidInput(activity, permissions)) return false; + + for (String permission : permissions) { + if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + return true; + } + + public static void reqPermission(Activity activity, String[] permissions, int requestCode) { + if (isValidInput(activity, permissions)) { + ActivityCompat.requestPermissions(activity, permissions, requestCode); + } + } + + public static boolean gotPermission(int[] grantResults) { + if (grantResults == null || grantResults.length == 0) return false; + + for (int result : grantResults) { + if (result != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + return true; + } + + public static String[] getPermission() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + ? new String[]{Manifest.permission.READ_MEDIA_IMAGES} + : new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}; + } + + private static boolean isInvalidInput(Activity activity, String[] permissions) { + return activity == null || permissions == null; + } + + private static boolean isValidInput(Activity activity, String[] permissions) { + if (IS_DEBUG) { + Log.d("PermissionUtil", "Validating input"); + } + return activity != null && permissions != null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/util/SpaceRepository.java b/app/src/main/java/com/ardrawing/ardrawingspace/util/SpaceRepository.java new file mode 100644 index 0000000..9a6bdb1 --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/util/SpaceRepository.java @@ -0,0 +1,21 @@ +package com.ardrawing.ardrawingspace.util; + +import android.app.Application; + +import com.ardrawing.ardrawingspace.room.SpaceEntity; +import com.ardrawing.ardrawingspace.viewmodel.SpaceViewModel; + +import java.util.List; + +public class SpaceRepository { + private final Application application; + + public SpaceRepository(Application application) { + this.application = application; + } + + public void insertAll(List entities) { + SpaceViewModel viewModel = new SpaceViewModel(application); + viewModel.insertAll(entities); + } +} diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/util/UploadUtil.java b/app/src/main/java/com/ardrawing/ardrawingspace/util/UploadUtil.java new file mode 100644 index 0000000..dc3f29f --- /dev/null +++ b/app/src/main/java/com/ardrawing/ardrawingspace/util/UploadUtil.java @@ -0,0 +1,156 @@ +package com.ardrawing.ardrawingspace.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.widget.Toast; + +import com.ardrawing.ardrawingspace.room.SpaceEntity; +import com.ardrawing.ardrawingspace.viewmodel.SpaceViewModel; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +public class UploadUtil { + private static final Handler mainHandler = new Handler(Looper.getMainLooper()); + + private static final boolean IS_DEBUG = false; + + public static void saveImageByPath(Uri imageUri, List imagePaths, SpaceViewModel viewModel, Context context) { + if (isInvalidInput(imageUri, imagePaths, viewModel, context)) return; + + try (InputStream inputStream = context.getContentResolver().openInputStream(imageUri)) { + Bitmap bitmap = decodeBitmapFromStream(inputStream); + if (bitmap == null) { + displayToast(context, "Unable to load image"); + return; + } + + File imageFile = createImageFile(context, generateUniqueImageName()); + saveBitmapToFile(bitmap, imageFile); + + String savedImagePath = imageFile.getAbsolutePath(); + executeImageSavingAsync(savedImagePath, imagePaths, viewModel, context, imageFile); + } catch (IOException e) { + displayToast(context, "Failed to save picture"); + } + } + + public static void deleteImageByPath(SpaceEntity spaceEntity, SpaceViewModel viewModel, Context context) { + if (isInvalidEntity(spaceEntity, viewModel, context)) return; + + File imageFile = new File(spaceEntity.getImagePath()); + if (!imageFile.exists()) { + displayToast(context, "Image does not exist"); + return; + } + + if (imageFile.delete()) { + viewModel.delete(spaceEntity); + displayToast(context, "Image deleted successfully"); + } else { + displayToast(context, "Failed to delete image"); + } + } + + private static void executeImageSavingAsync(String savedImagePath, List imagePaths, SpaceViewModel viewModel, Context context, File imageFile) { + new Thread(() -> { + if (checkImageDuplicate(savedImagePath, imagePaths, viewModel)) { + imageFile.delete(); + displayToast(context, "The image already exists"); + return; + } + + imagePaths.add(savedImagePath); + viewModel.insert(new SpaceEntity(savedImagePath, true, false)); + }).start(); + } + + private static boolean checkImageDuplicate(String imagePath, List imagePaths, SpaceViewModel viewModel) { + File newImageFile = new File(imagePath); + if (!newImageFile.exists()) return false; + + for (String path : imagePaths) { + File existingFile = new File(path); + if (compareFilesBySize(existingFile, newImageFile)) { + return true; + } + } + + List spaceEntityList = viewModel.getImport(); + for (SpaceEntity data : spaceEntityList) { + File existingFile = new File(data.getImagePath()); + if (compareFilesBySize(existingFile, newImageFile)) { + return true; + } + } + return false; + } + + private static boolean compareFilesBySize(File file1, File file2) { + return file1.exists() && file2.exists() && file1.length() == file2.length(); + } + + private static Bitmap decodeBitmapFromStream(InputStream inputStream) { + return BitmapFactory.decodeStream(inputStream); + } + + private static File createImageFile(Context context, String fileName) { + return new File(context.getFilesDir(), fileName + ".jpg"); + } + + private static void saveBitmapToFile(Bitmap bitmap, File file) throws IOException { + try (FileOutputStream outputStream = new FileOutputStream(file)) { + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); + } + } + + private static void displayToast(Context context, String message) { + if (isInvalidToastInput(context, message)) return; + mainHandler.post(() -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show()); + } + + private static boolean isInvalidToastInput(Context context, String message) { + if (IS_DEBUG) { + Log.d("UploadUtil", "Checking toast input"); + } + return context == null || message == null; + } + + private static boolean isInvalidInput(Uri imageUri, List imagePaths, SpaceViewModel viewModel, Context context) { + return imageUri == null || imagePaths == null || viewModel == null || context == null; + } + + private static boolean isInvalidEntity(SpaceEntity spaceEntity, SpaceViewModel viewModel, Context context) { + return spaceEntity == null || spaceEntity.getImagePath() == null || viewModel == null || context == null; + } + + private static String generateUniqueImageName() { + return String.valueOf(System.currentTimeMillis()); + } + + public static boolean isImageSizeValid(Uri imageUri, Context context) { + if (isInvalidSizeCheck(imageUri, context)) return false; + + try (InputStream inputStream = context.getContentResolver().openInputStream(imageUri)) { + if (inputStream == null) return false; + long imageSize = inputStream.available(); + long maxSize = 10 * 1024 * 1024; // 10MB + return imageSize <= maxSize; + } catch (IOException e) { + return false; + } + } + + private static boolean isInvalidSizeCheck(Uri imageUri, Context context) { + return imageUri == null || context == null; + } +} + diff --git a/app/src/main/java/com/ardrawing/ardrawingspace/viewmodel/SpaceViewModel.java b/app/src/main/java/com/ardrawing/ardrawingspace/viewmodel/SpaceViewModel.java index 7b9f185..156bb19 100644 --- a/app/src/main/java/com/ardrawing/ardrawingspace/viewmodel/SpaceViewModel.java +++ b/app/src/main/java/com/ardrawing/ardrawingspace/viewmodel/SpaceViewModel.java @@ -28,6 +28,10 @@ public class SpaceViewModel extends AndroidViewModel { executorService.execute(() -> dao.insert(spaceEntity)); } + public void insertAll(List spaceEntityList) { + executorService.execute(() -> dao.insertAll(spaceEntityList)); + } + public void delete(SpaceEntity spaceEntity) { executorService.execute(() -> dao.delete(spaceEntity)); } diff --git a/app/src/main/res/drawable/back.xml b/app/src/main/res/drawable/back.xml new file mode 100644 index 0000000..c086bff --- /dev/null +++ b/app/src/main/res/drawable/back.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/camera.xml b/app/src/main/res/drawable/camera.xml new file mode 100644 index 0000000..42fdaab --- /dev/null +++ b/app/src/main/res/drawable/camera.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/collection.xml b/app/src/main/res/drawable/collection.xml new file mode 100644 index 0000000..a946f8f --- /dev/null +++ b/app/src/main/res/drawable/collection.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/delete.xml b/app/src/main/res/drawable/delete.xml new file mode 100644 index 0000000..4110c16 --- /dev/null +++ b/app/src/main/res/drawable/delete.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/flash.xml b/app/src/main/res/drawable/flash.xml new file mode 100644 index 0000000..7cd4ba4 --- /dev/null +++ b/app/src/main/res/drawable/flash.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/home.xml b/app/src/main/res/drawable/home.xml new file mode 100644 index 0000000..e1cd473 --- /dev/null +++ b/app/src/main/res/drawable/home.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/layout_background.xml b/app/src/main/res/drawable/layout_background.xml new file mode 100644 index 0000000..5273c01 --- /dev/null +++ b/app/src/main/res/drawable/layout_background.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/like.xml b/app/src/main/res/drawable/like.xml new file mode 100644 index 0000000..daad08f --- /dev/null +++ b/app/src/main/res/drawable/like.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/photo.xml b/app/src/main/res/drawable/photo.xml new file mode 100644 index 0000000..f8348fa --- /dev/null +++ b/app/src/main/res/drawable/photo.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/progress_gradient.xml b/app/src/main/res/drawable/progress_gradient.xml new file mode 100644 index 0000000..1156788 --- /dev/null +++ b/app/src/main/res/drawable/progress_gradient.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/progress_thumb.xml b/app/src/main/res/drawable/progress_thumb.xml new file mode 100644 index 0000000..1b306d6 --- /dev/null +++ b/app/src/main/res/drawable/progress_thumb.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/resource_import.xml b/app/src/main/res/drawable/resource_import.xml new file mode 100644 index 0000000..0648d5d --- /dev/null +++ b/app/src/main/res/drawable/resource_import.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/rounded_rectangle.xml b/app/src/main/res/drawable/rounded_rectangle.xml new file mode 100644 index 0000000..6bc4853 --- /dev/null +++ b/app/src/main/res/drawable/rounded_rectangle.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/text_background.xml b/app/src/main/res/drawable/text_background.xml new file mode 100644 index 0000000..edb4323 --- /dev/null +++ b/app/src/main/res/drawable/text_background.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/un_collection.xml b/app/src/main/res/drawable/un_collection.xml new file mode 100644 index 0000000..608d222 --- /dev/null +++ b/app/src/main/res/drawable/un_collection.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/un_flash.xml b/app/src/main/res/drawable/un_flash.xml new file mode 100644 index 0000000..5a770a6 --- /dev/null +++ b/app/src/main/res/drawable/un_flash.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/un_home.xml b/app/src/main/res/drawable/un_home.xml new file mode 100644 index 0000000..6658b08 --- /dev/null +++ b/app/src/main/res/drawable/un_home.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/un_import.xml b/app/src/main/res/drawable/un_import.xml new file mode 100644 index 0000000..a3f1394 --- /dev/null +++ b/app/src/main/res/drawable/un_import.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/un_like.xml b/app/src/main/res/drawable/un_like.xml new file mode 100644 index 0000000..3f5b144 --- /dev/null +++ b/app/src/main/res/drawable/un_like.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/upload.xml b/app/src/main/res/drawable/upload.xml new file mode 100644 index 0000000..e8cc1a7 --- /dev/null +++ b/app/src/main/res/drawable/upload.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_category.xml b/app/src/main/res/layout/activity_category.xml new file mode 100644 index 0000000..23fc6d9 --- /dev/null +++ b/app/src/main/res/layout/activity_category.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_drawing.xml b/app/src/main/res/layout/activity_drawing.xml new file mode 100644 index 0000000..4c97294 --- /dev/null +++ b/app/src/main/res/layout/activity_drawing.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 5dfe6e2..33efca0 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,15 +5,42 @@ android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".activity.MainActivity"> + android:background="#F5F5F5" + tools:application=".activity.MainActivity"> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml new file mode 100644 index 0000000..cd50016 --- /dev/null +++ b/app/src/main/res/layout/activity_splash.xml @@ -0,0 +1,48 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_favorite.xml b/app/src/main/res/layout/fragment_favorite.xml new file mode 100644 index 0000000..e09dbe0 --- /dev/null +++ b/app/src/main/res/layout/fragment_favorite.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 0000000..a0b76e6 --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_import.xml b/app/src/main/res/layout/fragment_import.xml new file mode 100644 index 0000000..96dcc47 --- /dev/null +++ b/app/src/main/res/layout/fragment_import.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_space.xml b/app/src/main/res/layout/item_space.xml new file mode 100644 index 0000000..d47ffea --- /dev/null +++ b/app/src/main/res/layout/item_space.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/tab_custom.xml b/app/src/main/res/layout/tab_custom.xml new file mode 100644 index 0000000..60e6914 --- /dev/null +++ b/app/src/main/res/layout/tab_custom.xml @@ -0,0 +1,31 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 6f3b755..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..4563bda Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..a9f8c25 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..30cdabb Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..98323ec Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..adf1b5b Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/placeholder.png b/app/src/main/res/mipmap-xxxhdpi/placeholder.png new file mode 100644 index 0000000..422f674 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/placeholder.png differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c8524cd..a2bb384 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,4 +2,5 @@ #FF000000 #FFFFFFFF + #C0C0C0 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e6f7aeb..54fcedc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ AR Drawing Space + + Hello blank fragment \ No newline at end of file diff --git a/keystore.properties b/keystore.properties new file mode 100644 index 0000000..3d89bcc --- /dev/null +++ b/keystore.properties @@ -0,0 +1,6 @@ +app_name=AR Drawing Space +package_name=com.ardrawing.ardrawingspace +keystoreFile=app/ARDrawingSpace.jks +key_alias=ARDrawingSpacekey0 +key_store_password=ARDrawingSpace +key_password=ARDrawingSpace \ No newline at end of file