From 5bc930e96d13fc45656eeb439406db0342e19b22 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Sat, 25 Apr 2026 04:52:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=8A=E9=A1=B9=E7=9B=AEhttps://gitea.winbol?= =?UTF-8?q?l.cc/Studio/Gallery=5FBck20260425=5F042304=5F713.git=E7=A7=BB?= =?UTF-8?q?=E5=85=A5WinBoLL=E9=A1=B9=E7=9B=AE=E7=B3=BB=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gallery/README.md | 36 +++ gallery/app_update_description.txt | 1 + gallery/build.gradle | 119 +++++++++ gallery/build.properties | 8 + gallery/proguard-rules.pro | 137 ++++++++++ gallery/src/beta/AndroidManifest.xml | 12 + gallery/src/beta/res/values/strings.xml | 6 + gallery/src/main/AndroidManifest.xml | 58 +++++ .../java/cc/winboll/studio/gallery/Album.java | 25 ++ .../winboll/studio/gallery/AlbumActivity.java | 153 ++++++++++++ .../winboll/studio/gallery/AlbumAdapter.java | 79 ++++++ .../gallery/GlobalWinBoLLApplication.java | 44 ++++ .../winboll/studio/gallery/ImageAdapter.java | 76 ++++++ .../studio/gallery/ImagePagerAdapter.java | 56 +++++ .../studio/gallery/ImageViewerActivity.java | 219 ++++++++++++++++ .../winboll/studio/gallery/MainActivity.java | 234 ++++++++++++++++++ .../winboll/studio/gallery/Preferences.java | 33 +++ .../studio/gallery/SettingsActivity.java | 46 ++++ .../winboll/studio/gallery/TrashActivity.java | 193 +++++++++++++++ .../winboll/studio/gallery/TrashAdapter.java | 111 +++++++++ .../winboll/studio/gallery/TrashDbHelper.java | 78 ++++++ .../winboll/studio/gallery/TrashManager.java | 112 +++++++++ .../drawable-v24/ic_launcher_foreground.xml | 34 +++ gallery/src/main/res/drawable/ic_back.xml | 9 + gallery/src/main/res/drawable/ic_delete.xml | 9 + .../res/drawable/ic_launcher_background.xml | 170 +++++++++++++ gallery/src/main/res/drawable/ic_restore.xml | 9 + gallery/src/main/res/drawable/ic_share.xml | 9 + .../main/res/layout/activity_image_viewer.xml | 57 +++++ gallery/src/main/res/layout/activity_main.xml | 28 +++ .../src/main/res/layout/activity_settings.xml | 39 +++ gallery/src/main/res/layout/item_album.xml | 37 +++ gallery/src/main/res/layout/item_gallery.xml | 15 ++ .../src/main/res/layout/item_image_pager.xml | 14 ++ gallery/src/main/res/layout/item_trash.xml | 42 ++++ gallery/src/main/res/menu/menu_main.xml | 25 ++ gallery/src/main/res/menu/menu_trash.xml | 15 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes gallery/src/main/res/values/colors.xml | 8 + gallery/src/main/res/values/strings.xml | 19 ++ gallery/src/main/res/values/styles.xml | 11 + gallery/src/stage/AndroidManifest.xml | 12 + gallery/src/stage/res/values/strings.xml | 6 + settings.gradle-demo | 4 + 55 files changed, 2418 insertions(+) create mode 100644 gallery/README.md create mode 100644 gallery/app_update_description.txt create mode 100644 gallery/build.gradle create mode 100644 gallery/build.properties create mode 100644 gallery/proguard-rules.pro create mode 100644 gallery/src/beta/AndroidManifest.xml create mode 100644 gallery/src/beta/res/values/strings.xml create mode 100644 gallery/src/main/AndroidManifest.xml create mode 100644 gallery/src/main/java/cc/winboll/studio/gallery/Album.java create mode 100644 gallery/src/main/java/cc/winboll/studio/gallery/AlbumActivity.java create mode 100644 gallery/src/main/java/cc/winboll/studio/gallery/AlbumAdapter.java create mode 100644 gallery/src/main/java/cc/winboll/studio/gallery/GlobalWinBoLLApplication.java create mode 100644 gallery/src/main/java/cc/winboll/studio/gallery/ImageAdapter.java create mode 100644 gallery/src/main/java/cc/winboll/studio/gallery/ImagePagerAdapter.java create mode 100644 gallery/src/main/java/cc/winboll/studio/gallery/ImageViewerActivity.java create mode 100644 gallery/src/main/java/cc/winboll/studio/gallery/MainActivity.java create mode 100644 gallery/src/main/java/cc/winboll/studio/gallery/Preferences.java create mode 100644 gallery/src/main/java/cc/winboll/studio/gallery/SettingsActivity.java create mode 100644 gallery/src/main/java/cc/winboll/studio/gallery/TrashActivity.java create mode 100644 gallery/src/main/java/cc/winboll/studio/gallery/TrashAdapter.java create mode 100644 gallery/src/main/java/cc/winboll/studio/gallery/TrashDbHelper.java create mode 100644 gallery/src/main/java/cc/winboll/studio/gallery/TrashManager.java create mode 100644 gallery/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 gallery/src/main/res/drawable/ic_back.xml create mode 100644 gallery/src/main/res/drawable/ic_delete.xml create mode 100644 gallery/src/main/res/drawable/ic_launcher_background.xml create mode 100644 gallery/src/main/res/drawable/ic_restore.xml create mode 100644 gallery/src/main/res/drawable/ic_share.xml create mode 100644 gallery/src/main/res/layout/activity_image_viewer.xml create mode 100644 gallery/src/main/res/layout/activity_main.xml create mode 100644 gallery/src/main/res/layout/activity_settings.xml create mode 100644 gallery/src/main/res/layout/item_album.xml create mode 100644 gallery/src/main/res/layout/item_gallery.xml create mode 100644 gallery/src/main/res/layout/item_image_pager.xml create mode 100644 gallery/src/main/res/layout/item_trash.xml create mode 100644 gallery/src/main/res/menu/menu_main.xml create mode 100644 gallery/src/main/res/menu/menu_trash.xml create mode 100644 gallery/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 gallery/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 gallery/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 gallery/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 gallery/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 gallery/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 gallery/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 gallery/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 gallery/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 gallery/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 gallery/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 gallery/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 gallery/src/main/res/values/colors.xml create mode 100644 gallery/src/main/res/values/strings.xml create mode 100644 gallery/src/main/res/values/styles.xml create mode 100644 gallery/src/stage/AndroidManifest.xml create mode 100644 gallery/src/stage/res/values/strings.xml diff --git a/gallery/README.md b/gallery/README.md new file mode 100644 index 0000000..1606637 --- /dev/null +++ b/gallery/README.md @@ -0,0 +1,36 @@ +# AES +[![](https://jitpack.io/v/ZhanGSKen/AES.svg)](https://jitpack.io/#ZhanGSKen/AES) + +#### 介绍 +WinBoLL AndroidX 可视化元素类库。 + +#### 软件架构 +适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。 +也适配安卓应用 [AndroidIDE] 的 Gradle 编译结构。 + + +#### Gradle 编译说明 +调试版编译命令 :gradle assembleBetaDebug +阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh aes +阶段版类库发布命令 :git pull &&bash .winboll/bashPublishLIBAddTag.sh libaes + +#### 使用说明 + +#### 参与贡献 + +1. Fork 本仓库 +2. 新建 Feat_xxx 分支 +3. 提交代码 : ZhanGSKen(ZhanGSKen) +4. 新建 Pull Request + + +#### 特技 + +1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md +2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) +3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 +4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 +5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) +6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) + +#### 参考文档 diff --git a/gallery/app_update_description.txt b/gallery/app_update_description.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/gallery/app_update_description.txt @@ -0,0 +1 @@ + diff --git a/gallery/build.gradle b/gallery/build.gradle new file mode 100644 index 0000000..5c794eb --- /dev/null +++ b/gallery/build.gradle @@ -0,0 +1,119 @@ +apply plugin: 'com.android.application' +apply from: '../.winboll/winboll_app_build.gradle' +apply from: '../.winboll/winboll_lint_build.gradle' + +def genVersionName(def versionName){ + // 检查编译标志位配置 + assert (winbollBuildProps['stageCount'] != null) + assert (winbollBuildProps['baseVersion'] != null) + // 保存基础版本号 + winbollBuildProps.setProperty("baseVersion", "${versionName}"); + //保存编译标志配置 + FileOutputStream fos = new FileOutputStream(winbollBuildPropsFile) + winbollBuildProps.store(fos, "${winbollBuildPropsDesc}"); + fos.close(); + + // 返回编译版本号 + return "${versionName}." + winbollBuildProps['stageCount'] +} + +android { + // 适配MIUI12 + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + applicationId "cc.winboll.studio.gallery" + minSdkVersion 23 + // 适配MIUI12 + targetSdkVersion 30 + versionCode 1 + // versionName 更新后需要手动设置 + // .winboll/winbollBuildProps.properties 文件的 stageCount=0 + // Gradle编译环境下合起来的 versionName 就是 "${versionName}.0" + versionName "15.0" + if(true) { + versionName = genVersionName("${versionName}") + } + } + + // 米盟 SDK + packagingOptions { + doNotStrip "*/*/libmimo_1011.so" + } + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下 + } + } +} + +dependencies { + + api 'com.google.code.gson:gson:2.10.1' + + // 下拉控件 + api 'com.baoyz.pullrefreshlayout:library:1.2.0' + + // SSH + api 'com.jcraft:jsch:0.1.55' + // Html 解析 + api 'org.jsoup:jsoup:1.13.1' + // 二维码类库 + api 'com.google.zxing:core:3.4.1' + api 'com.journeyapps:zxing-android-embedded:3.6.0' + // 应用介绍页类库 + api 'io.github.medyo:android-about-page:2.0.0' + // 网络连接类库 + api 'com.squareup.okhttp3:okhttp:4.4.1' + // OkHttp网络请求 + implementation 'com.squareup.okhttp3:okhttp:3.14.9' + // FastJSON解析 + implementation 'com.alibaba:fastjson:1.2.76' + + // AndroidX 类库 + /*api 'androidx.appcompat:appcompat:1.1.0' + //api 'com.google.android.material:material:1.4.0' + //api 'androidx.viewpager:viewpager:1.0.0' + //api 'androidx.vectordrawable:vectordrawable:1.1.0' + //api 'androidx.vectordrawable:vectordrawable-animated:1.1.0' + //api 'androidx.fragment:fragment:1.1.0'*/ + + + // 米盟 + api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk + //注意:以下5个库必须要引入 + //implementation 'androidx.appcompat:appcompat:1.4.1' + api 'androidx.recyclerview:recyclerview:1.0.0' + api 'com.google.code.gson:gson:2.8.5' + api 'com.github.bumptech.glide:glide:4.9.0' + //annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0' + + implementation "androidx.annotation:annotation:1.3.0" + implementation "androidx.core:core:1.6.0" + implementation "androidx.drawerlayout:drawerlayout:1.1.1" + implementation "androidx.preference:preference:1.1.1" + implementation "androidx.viewpager:viewpager:1.0.0" + implementation "com.google.android.material:material:1.4.0" + implementation "com.google.guava:guava:24.1-jre" + /* + implementation "io.noties.markwon:core:$markwonVersion" + implementation "io.noties.markwon:ext-strikethrough:$markwonVersion" + implementation "io.noties.markwon:linkify:$markwonVersion" + implementation "io.noties.markwon:recycler:$markwonVersion" + */ + /*implementation 'com.termux:terminal-emulator:0.118.0' + implementation 'com.termux:terminal-view:0.118.0' + implementation 'com.termux:termux-shared:0.118.0' + */ + // WinBoLL库 nexus.winboll.cc 地址 + api 'cc.winboll.studio:libaes:15.15.9' + api 'cc.winboll.studio:libappbase:15.15.19' + + // WinBoLL备用库 jitpack.io 地址 + //api 'com.github.ZhanGSKen:AES:aes-v15.15.7' + //api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4' + + api fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/gallery/build.properties b/gallery/build.properties new file mode 100644 index 0000000..2f3e898 --- /dev/null +++ b/gallery/build.properties @@ -0,0 +1,8 @@ +#Created by .winboll/winboll_app_build.gradle +#Fri Apr 24 20:49:06 GMT 2026 +stageCount=0 +libraryProject= +baseVersion=15.0 +publishVersion=15.0.0 +buildCount=4 +baseBetaVersion=15.0.1 diff --git a/gallery/proguard-rules.pro b/gallery/proguard-rules.pro new file mode 100644 index 0000000..a18de74 --- /dev/null +++ b/gallery/proguard-rules.pro @@ -0,0 +1,137 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# ============================== 基础通用规则 ============================== +# 保留系统组件 +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference + +# 保留 WinBoLL 核心包及子类(合并简化规则) +-keep class cc.winboll.studio.** { *; } +-keepclassmembers class cc.winboll.studio.** { *; } + +# 保留所有类中的 public static final String TAG 字段(便于日志定位) +-keepclassmembers class * { + public static final java.lang.String TAG; +} + +# 保留序列化类(避免Parcelable/Gson解析异常) +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} + +# 保留 R 文件(避免资源ID混淆) +-keepclassmembers class **.R$* { + public static ; +} + +# 保留 native 方法(避免JNI调用失败) +-keepclasseswithmembernames class * { + native ; +} + +# 保留注解和泛型(避免反射/序列化异常) +-keepattributes *Annotation* +-keepattributes Signature + +# 屏蔽 Java 8+ 警告(适配 Java 7 语法) +-dontwarn java.lang.invoke.* +-dontwarn android.support.v8.renderscript.* +-dontwarn java.util.function.** + +# ============================== 第三方框架专项规则 ============================== +# OkHttp 4.4.1(米盟广告请求依赖,完善Lambda兼容) +-keep class okhttp3.** { *; } +-keep interface okhttp3.** { *; } +-keep class okhttp3.internal.** { *; } +-keep class okio.** { *; } +-dontwarn okhttp3.internal.platform.** +-dontwarn okio.** + +# Glide 4.9.0(米盟广告图片加载依赖) +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep public class * extends com.bumptech.glide.module.AppGlideModule +-keep public enum com.bumptech.glide.load.ImageHeaderParser$ImageType { + **[] $VALUES; + public *; +} +-keepclassmembers class * implements com.bumptech.glide.module.AppGlideModule { + (); +} +-dontwarn com.bumptech.glide.** + +# Gson 2.8.5(米盟广告数据序列化依赖) +-keep class com.google.gson.** { *; } +-keep interface com.google.gson.** { *; } +-keepclassmembers class * { + @com.google.gson.annotations.SerializedName ; +} + +# 米盟 SDK(核心广告组件,完整保留避免加载失败) +-keep class com.miui.zeus.** { *; } +-keep interface com.miui.zeus.** { *; } +# 保留米盟日志字段(便于广告加载失败排查) +-keepclassmembers class com.miui.zeus.mimo.sdk.** { + public static final java.lang.String TAG; +} + +# RecyclerView 1.0.0(米盟广告布局渲染依赖) +-keep class androidx.recyclerview.** { *; } +-keep interface androidx.recyclerview.** { *; } +-keepclassmembers class androidx.recyclerview.widget.RecyclerView$Adapter { + public *; +} + +# 其他第三方框架(按引入依赖保留,无则可删除) +# XXPermissions 18.63 +-keep class com.hjq.permissions.** { *; } +-keep interface com.hjq.permissions.** { *; } + +# ZXing 二维码(核心解析组件) +-keep class com.google.zxing.** { *; } +-keep class com.journeyapps.zxing.** { *; } + +# Jsoup HTML解析 +-keep class org.jsoup.** { *; } + +# Pinyin4j 拼音搜索 +-keep class net.sourceforge.pinyin4j.** { *; } + +# JSch SSH组件 +-keep class com.jcraft.jsch.** { *; } + +# AndroidX 基础组件 +-keep class androidx.appcompat.** { *; } +-keep interface androidx.appcompat.** { *; } + +# ============================== 优化与调试配置 ============================== +# 优化级别(平衡混淆效果与性能) +-optimizationpasses 5 +-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/* + +# 调试辅助(保留行号便于崩溃定位) +-verbose +-dontpreverify +-dontusemixedcaseclassnames +-keepattributes SourceFile,LineNumberTable + diff --git a/gallery/src/beta/AndroidManifest.xml b/gallery/src/beta/AndroidManifest.xml new file mode 100644 index 0000000..ee78d9f --- /dev/null +++ b/gallery/src/beta/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/gallery/src/beta/res/values/strings.xml b/gallery/src/beta/res/values/strings.xml new file mode 100644 index 0000000..9a532e7 --- /dev/null +++ b/gallery/src/beta/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + + Gallery+ + diff --git a/gallery/src/main/AndroidManifest.xml b/gallery/src/main/AndroidManifest.xml new file mode 100644 index 0000000..be26880 --- /dev/null +++ b/gallery/src/main/AndroidManifest.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/Album.java b/gallery/src/main/java/cc/winboll/studio/gallery/Album.java new file mode 100644 index 0000000..42aaa02 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/Album.java @@ -0,0 +1,25 @@ +package cc.winboll.studio.gallery; + +import android.net.Uri; +import cc.winboll.studio.libappbase.LogUtils; + +public class Album { + public static final String TAG = "Album"; + private String name; + private String path; + private Uri coverUri; + private int imageCount; + + public Album(String name, String path, Uri coverUri, int imageCount) { + LogUtils.d(TAG, "Album created: " + name); + this.name = name; + this.path = path; + this.coverUri = coverUri; + this.imageCount = imageCount; + } + + public String getName() { return name; } + public String getPath() { return path; } + public Uri getCoverUri() { return coverUri; } + public int getImageCount() { return imageCount; } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/AlbumActivity.java b/gallery/src/main/java/cc/winboll/studio/gallery/AlbumActivity.java new file mode 100644 index 0000000..9f5dc4d --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/AlbumActivity.java @@ -0,0 +1,153 @@ +package cc.winboll.studio.gallery; + +import android.content.ContentResolver; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import java.util.ArrayList; + +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.gallery.ImageAdapter.OnImageClickListener; + +public class AlbumActivity extends AppCompatActivity { + public static final String TAG = "AlbumActivity"; + private static final int PERMISSION_REQUEST_CODE = 101; + public static final String EXTRA_ALBUM_PATH = "album_path"; + public static final String EXTRA_ALBUM_NAME = "album_name"; + private RecyclerView recyclerView; + private ImageAdapter adapter; + private String albumPath; + private String albumName; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + LogUtils.d(TAG, "onCreate"); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH); + albumName = getIntent().getStringExtra(EXTRA_ALBUM_NAME); + + getSupportActionBar().setTitle(albumName); + + recyclerView = findViewById(R.id.recycler_view); + recyclerView.setLayoutManager(new GridLayoutManager(this, 3)); + adapter = new ImageAdapter(); + recyclerView.setAdapter(adapter); + + adapter.setOnImageClickListener(new OnImageClickListener() { + @Override + public void onImageClick(int position, ArrayList urls, ArrayList paths) { + Intent intent = new Intent(AlbumActivity.this, ImageViewerActivity.class); + intent.putParcelableArrayListExtra(ImageViewerActivity.EXTRA_IMAGE_URLS, urls); + intent.putStringArrayListExtra(ImageViewerActivity.EXTRA_POSITIONS, paths); + intent.putExtra(ImageViewerActivity.EXTRA_POSITION, position); + startActivity(intent); + } + }); + + if (checkPermission()) { + loadImages(); + } else { + requestPermission(); + } + } + + private boolean checkPermission() { + return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED; + } + + private void requestPermission() { + ActivityCompat.requestPermissions(this, + new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE}, + PERMISSION_REQUEST_CODE); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == PERMISSION_REQUEST_CODE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + loadImages(); + } else { + Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show(); + } + } + } + + private void loadImages() { + LogUtils.d(TAG, "loadImages"); + ArrayList imageUrls = new ArrayList<>(); + ArrayList imagePaths = new ArrayList<>(); + ContentResolver contentResolver = getContentResolver(); + Uri collection = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + + String selection = android.provider.MediaStore.Images.Media.DATA + " LIKE ?"; + String[] selectionArgs = new String[]{albumPath + "%"}; + String sortOrder = android.provider.MediaStore.Images.Media.DATE_ADDED + " DESC"; + + try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) { + if (cursor != null) { + int dataColumn = cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media.DATA); + while (cursor.moveToNext()) { + String path = cursor.getString(dataColumn); + if (path != null && path.startsWith(albumPath + "/")) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow(android.provider.MediaStore.Images.Media._ID)); + Uri contentUri = Uri.withAppendedPath(collection, String.valueOf(id)); + imageUrls.add(contentUri); + imagePaths.add(path); + } + } + } + } + + if (imageUrls.isEmpty()) { + Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show(); + LogUtils.i(TAG, "No images found"); + } + adapter.setData(imageUrls, imagePaths); + LogUtils.d(TAG, "Loaded " + imageUrls.size() + " images"); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_refresh) { + if (checkPermission()) { + loadImages(); + } + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onResume() { + super.onResume(); + if (checkPermission()) { + loadImages(); + } + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/AlbumAdapter.java b/gallery/src/main/java/cc/winboll/studio/gallery/AlbumAdapter.java new file mode 100644 index 0000000..4087a60 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/AlbumAdapter.java @@ -0,0 +1,79 @@ +package cc.winboll.studio.gallery; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import java.util.ArrayList; + +import cc.winboll.studio.libappbase.LogUtils; + +public class AlbumAdapter extends RecyclerView.Adapter { + public static final String TAG = "AlbumAdapter"; + private ArrayList albums = new ArrayList<>(); + private OnAlbumClickListener listener; + + public interface OnAlbumClickListener { + void onAlbumClick(Album album); + } + + public void setOnAlbumClickListener(OnAlbumClickListener listener) { + this.listener = listener; + } + + public void setData(ArrayList albums) { + this.albums = albums; + LogUtils.d(TAG, "setData: " + albums.size() + " albums"); + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_album, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, final int position) { + final Album album = albums.get(position); + Glide.with(holder.coverImage.getContext()) + .load(album.getCoverUri()) + .centerCrop() + .into(holder.coverImage); + holder.albumName.setText(album.getName()); + holder.imageCount.setText(album.getImageCount() + " photos"); + + holder.itemView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + listener.onAlbumClick(album); + } + } + }); + } + + @Override + public int getItemCount() { + return albums.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + ImageView coverImage; + TextView albumName; + TextView imageCount; + ViewHolder(View itemView) { + super(itemView); + coverImage = itemView.findViewById(R.id.album_cover); + albumName = itemView.findViewById(R.id.album_name); + imageCount = itemView.findViewById(R.id.image_count); + } + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/GlobalWinBoLLApplication.java b/gallery/src/main/java/cc/winboll/studio/gallery/GlobalWinBoLLApplication.java new file mode 100644 index 0000000..c48b23b --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/GlobalWinBoLLApplication.java @@ -0,0 +1,44 @@ +package cc.winboll.studio.gallery; + +import cc.winboll.studio.libaes.utils.WinBoLLActivityManager; +import cc.winboll.studio.libappbase.GlobalApplication; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.ToastUtils; + +/** + * @Author 豆包&ZhanGSKen + * @Date 2026/04/24 15:23 + */ +public class GlobalWinBoLLApplication extends GlobalApplication { + + public static final String TAG = "GlobalWinBoLLApplication"; + + +@Override + public void onCreate() { + super.onCreate(); + LogUtils.d(TAG, "onCreate"); + setIsDebugging(BuildConfig.DEBUG); + //setIsDebugging(false); + + WinBoLLActivityManager.init(this); + + // 初始化 Toast 框架 + ToastUtils.init(this); + // 设置 Toast 布局样式 + //ToastUtils.setView(R.layout.view_toast); + //ToastUtils.setStyle(new WhiteToastStyle()); + //ToastUtils.setGravity(Gravity.BOTTOM, 0, 200); + + //CrashHandler.getInstance().registerGlobal(this); + //CrashHandler.getInstance().registerPart(this); + } + +@Override + public void onTerminate() { + super.onTerminate(); + LogUtils.d(TAG, "onTerminate"); + ToastUtils.release(); + } + +} diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/ImageAdapter.java b/gallery/src/main/java/cc/winboll/studio/gallery/ImageAdapter.java new file mode 100644 index 0000000..20a566d --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/ImageAdapter.java @@ -0,0 +1,76 @@ +package cc.winboll.studio.gallery; + +import android.net.Uri; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import java.util.ArrayList; + +import cc.winboll.studio.libappbase.LogUtils; + +public class ImageAdapter extends RecyclerView.Adapter { + public static final String TAG = "ImageAdapter"; + private ArrayList imageUrls = new ArrayList<>(); + private ArrayList imagePaths = new ArrayList<>(); + private OnImageClickListener listener; + + public interface OnImageClickListener { + void onImageClick(int position, ArrayList urls, ArrayList paths); + } + + public void setOnImageClickListener(OnImageClickListener listener) { + this.listener = listener; + } + + public void setData(ArrayList urls, ArrayList paths) { + this.imageUrls = urls; + this.imagePaths = paths; + LogUtils.d(TAG, "setData: " + urls.size() + " images"); + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_gallery, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, final int position) { + Glide.with(holder.imageView.getContext()) + .load(imageUrls.get(position)) + .centerCrop() + .into(holder.imageView); + + final ArrayList urls = imageUrls; + final ArrayList paths = imagePaths; + holder.imageView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + listener.onImageClick(position, urls, paths); + } + } + }); + } + + @Override + public int getItemCount() { + return imageUrls.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + ImageView imageView; + ViewHolder(View itemView) { + super(itemView); + imageView = itemView.findViewById(R.id.image); + } + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/ImagePagerAdapter.java b/gallery/src/main/java/cc/winboll/studio/gallery/ImagePagerAdapter.java new file mode 100644 index 0000000..b7942da --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/ImagePagerAdapter.java @@ -0,0 +1,56 @@ +package cc.winboll.studio.gallery; + +import android.net.Uri; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import androidx.annotation.NonNull; +import androidx.viewpager.widget.PagerAdapter; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import java.util.ArrayList; + +import cc.winboll.studio.libappbase.LogUtils; + +public class ImagePagerAdapter extends PagerAdapter { + public static final String TAG = "ImagePagerAdapter"; + private ArrayList imageUrls; + + public ImagePagerAdapter(ArrayList imageUrls) { + this.imageUrls = imageUrls; + LogUtils.d(TAG, "ImagePagerAdapter created with " + imageUrls.size() + " images"); + } + + @Override + public int getCount() { + return imageUrls.size(); + } + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, int position) { + View view = LayoutInflater.from(container.getContext()) + .inflate(R.layout.item_image_pager, container, false); + ImageView imageView = view.findViewById(R.id.image); + + Glide.with(imageView.getContext()) + .load(imageUrls.get(position)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .into(imageView); + + container.addView(view); + return view; + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + container.removeView((View) object); + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return view == object; + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/ImageViewerActivity.java b/gallery/src/main/java/cc/winboll/studio/gallery/ImageViewerActivity.java new file mode 100644 index 0000000..445be56 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/ImageViewerActivity.java @@ -0,0 +1,219 @@ +package cc.winboll.studio.gallery; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.MediaStore; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.view.WindowManager; +import android.widget.ImageButton; +import androidx.viewpager.widget.ViewPager; +import java.io.File; +import java.util.ArrayList; + +import cc.winboll.studio.libappbase.LogUtils; + +public class ImageViewerActivity extends Activity implements ViewPager.OnPageChangeListener { + public static final String TAG = "ImageViewerActivity"; + public static final String EXTRA_IMAGE_URLS = "image_urls"; + public static final String EXTRA_POSITIONS = "image_positions"; + public static final String EXTRA_POSITION = "position"; + + private ArrayList imageUrls; + private ArrayList imagePaths; + private int currentPosition; + private ViewPager viewPager; + private View toolbar; + private ImageButton btnBack; + private ImageButton btnDelete; + private ImageButton btnShare; + private GestureDetector gestureDetector; + private TrashManager trashManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + setContentView(R.layout.activity_image_viewer); + LogUtils.d(TAG, "onCreate"); + + imageUrls = getIntent().getParcelableArrayListExtra(EXTRA_IMAGE_URLS); + imagePaths = getIntent().getStringArrayListExtra(EXTRA_POSITIONS); + currentPosition = getIntent().getIntExtra(EXTRA_POSITION, 0); + + trashManager = new TrashManager(this); + + viewPager = findViewById(R.id.view_pager); + toolbar = findViewById(R.id.toolbar); + btnBack = findViewById(R.id.btn_back); + btnDelete = findViewById(R.id.btn_delete); + btnShare = findViewById(R.id.btn_share); + + ImagePagerAdapter adapter = new ImagePagerAdapter(imageUrls); + viewPager.setAdapter(adapter); + viewPager.setCurrentItem(currentPosition); + viewPager.addOnPageChangeListener(this); + + gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + toggleToolbar(); + return true; + } + }); + + viewPager.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + return gestureDetector.onTouchEvent(event); + } + }); + + btnBack.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + + btnDelete.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showDeleteDialog(); + } + }); + + btnShare.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + shareCurrentImage(); + } + }); + } + + private void toggleToolbar() { + if (toolbar.getVisibility() == View.VISIBLE) { + toolbar.setVisibility(View.GONE); + } else { + toolbar.setVisibility(View.VISIBLE); + } + } + + private void showDeleteDialog() { + new AlertDialog.Builder(this) + .setMessage("Delete to trash?") + .setPositiveButton("Yes", new android.content.DialogInterface.OnClickListener() { + @Override + public void onClick(android.content.DialogInterface dialog, int which) { + moveToTrash(); + } + }) + .setNegativeButton("No", null) + .show(); + } + + private void moveToTrash() { + LogUtils.d(TAG, "moveToTrash"); + if (currentPosition >= 0 && currentPosition < imageUrls.size()) { + String imagePath = ""; + if (imagePaths != null && currentPosition < imagePaths.size()) { + imagePath = imagePaths.get(currentPosition); + } else { + imagePath = getPathFromUri(imageUrls.get(currentPosition)); + } + + Uri imageUri = imageUrls.get(currentPosition); + long result = -1; + + if (!imagePath.isEmpty()) { + result = trashManager.addToTrash(imagePath); + } + + if (result > 0) { + try { + getContentResolver().delete(imageUri, null, null); + } catch (Exception e) { + e.printStackTrace(); + } + android.widget.Toast.makeText(this, "Moved to trash", android.widget.Toast.LENGTH_SHORT).show(); + LogUtils.i(TAG, "Moved to trash"); + removeCurrentImage(); + } else { + try { + int deleted = getContentResolver().delete(imageUri, null, null); + android.widget.Toast.makeText(this, "Deleted: " + deleted, android.widget.Toast.LENGTH_SHORT).show(); + removeCurrentImage(); + } catch (Exception e) { + e.printStackTrace(); + removeCurrentImage(); + } + } + } + } + + private void removeCurrentImage() { + imageUrls.remove(currentPosition); + if (imagePaths != null) { + imagePaths.remove(currentPosition); + } + + if (imageUrls.isEmpty()) { + finish(); + } else { + if (currentPosition >= imageUrls.size()) { + currentPosition = imageUrls.size() - 1; + } + viewPager.setAdapter(new ImagePagerAdapter(imageUrls)); + viewPager.setCurrentItem(currentPosition); + } + } + + private String getPathFromUri(Uri uri) { + String[] projection = { MediaStore.Images.Media.DATA }; + android.database.Cursor cursor = getContentResolver().query(uri, projection, null, null, null); + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); + return cursor.getString(columnIndex); + } + } finally { + cursor.close(); + } + } + return uri.getPath(); + } + + private void shareCurrentImage() { + if (currentPosition >= 0 && currentPosition < imageUrls.size()) { + Uri imageUri = imageUrls.get(currentPosition); + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("image/*"); + shareIntent.putExtra(Intent.EXTRA_STREAM, imageUri); + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(Intent.createChooser(shareIntent, "Share Image")); + } + } + + @Override + public void onPageSelected(int position) { + currentPosition = position; + } + + @Override + public void onPageScrollStateChanged(int state) {} + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {} + + @Override + public void onBackPressed() { + finish(); + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/MainActivity.java b/gallery/src/main/java/cc/winboll/studio/gallery/MainActivity.java new file mode 100644 index 0000000..86dec88 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/MainActivity.java @@ -0,0 +1,234 @@ +package cc.winboll.studio.gallery; + +import android.Manifest; +import android.content.ContentResolver; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.provider.MediaStore; +import android.provider.Settings; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import cc.winboll.studio.gallery.AlbumAdapter.OnAlbumClickListener; +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.LogActivity; +import java.io.File; +import java.io.FileFilter; +import java.util.ArrayList; + +public class MainActivity extends AppCompatActivity { + public static final String TAG = "MainActivity"; + private static final int PERMISSION_REQUEST_CODE = 100; + private static final int MANAGE_PERMISSION_REQUEST_CODE = 101; + private RecyclerView recyclerView; + private AlbumAdapter adapter; + private Preferences prefs; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + LogUtils.d(TAG, "onCreate"); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + prefs = new Preferences(this); + + recyclerView = findViewById(R.id.recycler_view); + recyclerView.setLayoutManager(new GridLayoutManager(this, 2)); + adapter = new AlbumAdapter(); + recyclerView.setAdapter(adapter); + + adapter.setOnAlbumClickListener(new OnAlbumClickListener() { + @Override + public void onAlbumClick(Album album) { + Intent intent = new Intent(MainActivity.this, AlbumActivity.class); + intent.putExtra(AlbumActivity.EXTRA_ALBUM_PATH, album.getPath()); + intent.putExtra(AlbumActivity.EXTRA_ALBUM_NAME, album.getName()); + startActivity(intent); + } + }); + + checkAndRequestPermissions(); + } + + private void checkAndRequestPermissions() { + LogUtils.i(TAG, "checkAndRequestPermissions"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (!Environment.isExternalStorageManager()) { + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivityForResult(intent, MANAGE_PERMISSION_REQUEST_CODE); + } catch (Exception e) { + Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); + startActivityForResult(intent, MANAGE_PERMISSION_REQUEST_CODE); + } + return; + } + } + + if (checkPermission()) { + loadAlbums(); + } else { + requestPermission(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == MANAGE_PERMISSION_REQUEST_CODE) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (Environment.isExternalStorageManager()) { + loadAlbums(); + } else { + Toast.makeText(this, "Permission required", Toast.LENGTH_SHORT).show(); + } + } + } + } + + private boolean checkPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return Environment.isExternalStorageManager(); + } + return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED; + } + + private void requestPermission() { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + PERMISSION_REQUEST_CODE); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == PERMISSION_REQUEST_CODE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + loadAlbums(); + } else { + Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show(); + } + } + } + + private void loadAlbums() { + LogUtils.d(TAG, "loadAlbums"); + String folderPath = prefs.getFolderPath(); + File baseFolder = new File(folderPath); + + if (!baseFolder.exists() || !baseFolder.isDirectory()) { + folderPath = Environment.getExternalStorageDirectory() + "/DCIM"; + baseFolder = new File(folderPath); + if (!baseFolder.exists()) { + folderPath = Environment.getExternalStorageDirectory() + "/Pictures"; + baseFolder = new File(folderPath); + } + } + + ArrayList albums = new ArrayList<>(); + + FileFilter directoryFilter = new FileFilter() { + @Override + public boolean accept(File file) { + return file.isDirectory(); + } + }; + File[] subfolders = baseFolder.listFiles(directoryFilter); + if (subfolders != null) { + for (File subfolder : subfolders) { + ArrayList images = getImagesInFolder(subfolder.getAbsolutePath()); + if (!images.isEmpty()) { + Uri latestImage = images.get(0); + albums.add(new Album(subfolder.getName(), subfolder.getAbsolutePath(), latestImage, images.size())); + } + } + } + + if (albums.isEmpty()) { + Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show(); + LogUtils.i(TAG, "No albums found"); + } + adapter.setData(albums); + LogUtils.d(TAG, "Loaded " + albums.size() + " albums"); + } + + private ArrayList getImagesInFolder(String folderPath) { + ArrayList imageUrls = new ArrayList<>(); + ContentResolver contentResolver = getContentResolver(); + Uri collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + + String selection = MediaStore.Images.Media.DATA + " LIKE ?"; + String[] selectionArgs = new String[]{folderPath + "/%"}; + String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC"; + + try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) { + if (cursor != null) { + int dataColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); + while (cursor.moveToNext()) { + String path = cursor.getString(dataColumn); + if (path != null) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)); + Uri contentUri = Uri.withAppendedPath(collection, String.valueOf(id)); + imageUrls.add(contentUri); + } + } + } + } + return imageUrls; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + int id = item.getItemId(); + if (id == R.id.action_settings) { + startActivity(new Intent(this, SettingsActivity.class)); + return true; + } else if (id == R.id.action_trash) { + startActivity(new Intent(this, TrashActivity.class)); + return true; + } else if (id == R.id.action_refresh) { + if (checkPermission()) { + loadAlbums(); + } + return true; + } else if (id == R.id.action_debug) { + LogActivity.startLogActivity(this); +// Log.d("Gallery", "Debug log message"); +// Toast.makeText(this, R.string.debug_message, Toast.LENGTH_SHORT).show(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onResume() { + super.onResume(); + if (checkPermission()) { + loadAlbums(); + } + } +} diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/Preferences.java b/gallery/src/main/java/cc/winboll/studio/gallery/Preferences.java new file mode 100644 index 0000000..809391d --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/Preferences.java @@ -0,0 +1,33 @@ +package cc.winboll.studio.gallery; + +import android.content.Context; +import android.content.SharedPreferences; +import cc.winboll.studio.libappbase.LogUtils; + +public class Preferences { + public static final String TAG = "Preferences"; + private static final String PREF_NAME = "gallery_prefs"; + private static final String KEY_FOLDER_PATH = "folder_path"; + private static final String DEFAULT_PATH = "/storage/emulated/0/DCIM"; + + public static String getDefaultPath() { + return DEFAULT_PATH; + } + + private final SharedPreferences prefs; + + public Preferences(Context context) { + prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + } + + public String getFolderPath() { + String path = prefs.getString(KEY_FOLDER_PATH, DEFAULT_PATH); + LogUtils.d(TAG, "getFolderPath: " + path); + return path; + } + + public void setFolderPath(String path) { + LogUtils.d(TAG, "setFolderPath: " + path); + prefs.edit().putString(KEY_FOLDER_PATH, path).apply(); + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/SettingsActivity.java b/gallery/src/main/java/cc/winboll/studio/gallery/SettingsActivity.java new file mode 100644 index 0000000..c730c73 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/SettingsActivity.java @@ -0,0 +1,46 @@ +package cc.winboll.studio.gallery; + +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; +import cc.winboll.studio.libappbase.LogUtils; + +public class SettingsActivity extends AppCompatActivity { + public static final String TAG = "SettingsActivity"; + private Preferences prefs; + private EditText editFolderPath; + private TextView textCurrentPath; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + LogUtils.d(TAG, "onCreate"); + + prefs = new Preferences(this); + + editFolderPath = findViewById(R.id.edit_folder_path); + textCurrentPath = findViewById(R.id.text_current_path); + Button btnSave = findViewById(R.id.btn_save); + + String currentPath = prefs.getFolderPath(); + editFolderPath.setText(currentPath); + textCurrentPath.setText("Current: " + currentPath); + + btnSave.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + String newPath = editFolderPath.getText().toString().trim(); + if (!newPath.isEmpty()) { + prefs.setFolderPath(newPath); + textCurrentPath.setText("Current: " + newPath); + } + finish(); + } + }); + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/TrashActivity.java b/gallery/src/main/java/cc/winboll/studio/gallery/TrashActivity.java new file mode 100644 index 0000000..ab30395 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/TrashActivity.java @@ -0,0 +1,193 @@ +package cc.winboll.studio.gallery; + +import android.content.ContentResolver; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.MediaStore; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import java.io.File; +import java.util.ArrayList; + +import cc.winboll.studio.libappbase.LogUtils; + +public class TrashActivity extends AppCompatActivity { + public static final String TAG = "TrashActivity"; + private static final int PERMISSION_REQUEST_CODE = 102; + private RecyclerView recyclerView; + private TrashAdapter adapter; + private TrashManager trashManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + LogUtils.d(TAG, "onCreate"); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setTitle("Trash"); + + trashManager = new TrashManager(this); + + recyclerView = findViewById(R.id.recycler_view); + recyclerView.setLayoutManager(new GridLayoutManager(this, 3)); + adapter = new TrashAdapter(); + recyclerView.setAdapter(adapter); + + adapter.setOnTrashClickListener(new TrashAdapter.OnTrashClickListener() { + @Override + public void onRestoreClick(int position) { + restoreImage(position); + } + + @Override + public void onDeleteClick(int position) { + permanentlyDelete(position); + } + }); + + if (checkPermission()) { + loadTrash(); + } else { + requestPermission(); + } + } + + private boolean checkPermission() { + return ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED; + } + + private void requestPermission() { + ActivityCompat.requestPermissions(this, + new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE}, + PERMISSION_REQUEST_CODE); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == PERMISSION_REQUEST_CODE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + loadTrash(); + } else { + Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show(); + } + } + } + + private void loadTrash() { + LogUtils.d(TAG, "loadTrash"); + Cursor cursor = trashManager.getTrashList(); + ArrayList items = new ArrayList(); + ArrayList uris = new ArrayList(); + + String trashPath = TrashDbHelper.getTrashPath(); + File trashDir = new File(trashPath); + + if (cursor != null && cursor.getCount() > 0) { + cursor.moveToFirst(); + do { + try { + long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); + String fileName = cursor.getString(cursor.getColumnIndexOrThrow("file_name")); + String originalPath = cursor.getString(cursor.getColumnIndexOrThrow("original_path")); + String originalFolder = cursor.getString(cursor.getColumnIndexOrThrow("original_folder")); + + TrashItem item = new TrashItem(); + item.id = id; + item.fileName = fileName; + item.originalPath = originalPath; + item.originalFolder = originalFolder; + + File trashFile = new File(trashDir, fileName); + if (trashFile.exists()) { + items.add(item); + uris.add(Uri.fromFile(trashFile)); + } + } catch (Exception e) { + e.printStackTrace(); + } + } while (cursor.moveToNext()); + cursor.close(); + } + + adapter.setData(items, uris); + + if (items.isEmpty()) { + Toast.makeText(this, "Trash is empty", Toast.LENGTH_SHORT).show(); + } + } + + private void restoreImage(int position) { + LogUtils.d(TAG, "restoreImage: " + position); + long id = adapter.getItemId(position); + String fileName = adapter.getFileName(position); + String originalPath = adapter.getOriginalPath(position); + + if (trashManager.restore(id, fileName, originalPath)) { + Toast.makeText(this, "Image restored", Toast.LENGTH_SHORT).show(); + LogUtils.i(TAG, "Image restored"); + loadTrash(); + } else { + Toast.makeText(this, "Restore failed", Toast.LENGTH_SHORT).show(); + } + } + + private void permanentlyDelete(int position) { + long id = adapter.getItemId(position); + String fileName = adapter.getFileName(position); + + if (trashManager.deletePermanently(id, fileName)) { + Toast.makeText(this, "Image deleted", Toast.LENGTH_SHORT).show(); + loadTrash(); + } else { + Toast.makeText(this, "Delete failed", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_trash, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.action_clear_trash) { + trashManager.clearTrash(); + Toast.makeText(this, "Trash cleared", Toast.LENGTH_SHORT).show(); + loadTrash(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onResume() { + super.onResume(); + if (checkPermission()) { + loadTrash(); + } + } + + public static class TrashItem { + public long id; + public String fileName; + public String originalPath; + public String originalFolder; + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/TrashAdapter.java b/gallery/src/main/java/cc/winboll/studio/gallery/TrashAdapter.java new file mode 100644 index 0000000..49a7cae --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/TrashAdapter.java @@ -0,0 +1,111 @@ +package cc.winboll.studio.gallery; + +import android.net.Uri; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import java.util.ArrayList; + +import cc.winboll.studio.libappbase.LogUtils; + +public class TrashAdapter extends RecyclerView.Adapter { + public static final String TAG = "TrashAdapter"; + private ArrayList trashItems = new ArrayList(); + private ArrayList imageUrls = new ArrayList(); + private OnTrashClickListener listener; + + public interface OnTrashClickListener { + void onRestoreClick(int position); + void onDeleteClick(int position); + } + + public void setOnTrashClickListener(OnTrashClickListener listener) { + this.listener = listener; + } + + public void setData(ArrayList items, ArrayList uris) { + this.trashItems = items; + this.imageUrls = uris; + LogUtils.d(TAG, "setData: " + items.size() + " items"); + notifyDataSetChanged(); + } + + public long getItemId(int position) { + if (position >= 0 && position < trashItems.size()) { + return trashItems.get(position).id; + } + return -1; + } + + public String getFileName(int position) { + if (position >= 0 && position < trashItems.size()) { + return trashItems.get(position).fileName; + } + return ""; + } + + public String getOriginalPath(int position) { + if (position >= 0 && position < trashItems.size()) { + return trashItems.get(position).originalPath; + } + return ""; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_trash, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, final int position) { + if (position < imageUrls.size()) { + Glide.with(holder.imageView.getContext()) + .load(imageUrls.get(position)) + .centerCrop() + .into(holder.imageView); + } + + holder.btnRestore.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + listener.onRestoreClick(position); + } + } + }); + + holder.btnDelete.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + listener.onDeleteClick(position); + } + } + }); + } + + @Override + public int getItemCount() { + return trashItems.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + ImageView imageView; + ImageView btnRestore; + ImageView btnDelete; + ViewHolder(View itemView) { + super(itemView); + imageView = itemView.findViewById(R.id.image); + btnRestore = itemView.findViewById(R.id.btn_restore); + btnDelete = itemView.findViewById(R.id.btn_delete); + } + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/TrashDbHelper.java b/gallery/src/main/java/cc/winboll/studio/gallery/TrashDbHelper.java new file mode 100644 index 0000000..6d5cd11 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/TrashDbHelper.java @@ -0,0 +1,78 @@ +package cc.winboll.studio.gallery; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.os.Environment; +import java.io.File; + +import cc.winboll.studio.libappbase.LogUtils; + +public class TrashDbHelper extends SQLiteOpenHelper { + public static final String TAG = "TrashDbHelper"; + private static final String DB_NAME = "trash.db"; + private static final int DB_VERSION = 1; + private static final String TABLE_NAME = "trash_items"; + private static final String COL_ID = "_id"; + private static final String COL_FILE_NAME = "file_name"; + private static final String COL_ORIGINAL_PATH = "original_path"; + private static final String COL_ORIGINAL_FOLDER = "original_folder"; + private static final String COL_DELETE_TIME = "delete_time"; + + public TrashDbHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + LogUtils.d(TAG, "onCreate"); + db.execSQL("CREATE TABLE " + TABLE_NAME + " (" + + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + COL_FILE_NAME + " TEXT, " + + COL_ORIGINAL_PATH + " TEXT, " + + COL_ORIGINAL_FOLDER + " TEXT, " + + COL_DELETE_TIME + " INTEGER)"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + LogUtils.i(TAG, "onUpgrade: " + oldVersion + " -> " + newVersion); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(db); + } + + public long insert(String fileName, String originalPath, String originalFolder) { + SQLiteDatabase db = getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(COL_FILE_NAME, fileName); + values.put(COL_ORIGINAL_PATH, originalPath); + values.put(COL_ORIGINAL_FOLDER, originalFolder); + values.put(COL_DELETE_TIME, System.currentTimeMillis()); + return db.insert(TABLE_NAME, null, values); + } + + public Cursor getAll() { + SQLiteDatabase db = getReadableDatabase(); + return db.query(TABLE_NAME, null, null, null, null, null, COL_DELETE_TIME + " DESC"); + } + + public int delete(long id) { + SQLiteDatabase db = getWritableDatabase(); + return db.delete(TABLE_NAME, COL_ID + "=?", new String[]{String.valueOf(id)}); + } + + public void clear() { + SQLiteDatabase db = getWritableDatabase(); + db.delete(TABLE_NAME, null, null); + } + + public static String getTrashPath() { + File trashDir = new File(Environment.getExternalStorageDirectory(), ".Trash"); + if (!trashDir.exists()) { + trashDir.mkdirs(); + } + return trashDir.getAbsolutePath(); + } +} \ No newline at end of file diff --git a/gallery/src/main/java/cc/winboll/studio/gallery/TrashManager.java b/gallery/src/main/java/cc/winboll/studio/gallery/TrashManager.java new file mode 100644 index 0000000..2c8bc35 --- /dev/null +++ b/gallery/src/main/java/cc/winboll/studio/gallery/TrashManager.java @@ -0,0 +1,112 @@ +package cc.winboll.studio.gallery; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore; +import java.io.File; +import java.util.UUID; + +import cc.winboll.studio.libappbase.LogUtils; + +public class TrashManager { + public static final String TAG = "TrashManager"; + private final Context context; + private final TrashDbHelper dbHelper; + + public TrashManager(Context context) { + LogUtils.d(TAG, "TrashManager created"); + this.context = context; + this.dbHelper = new TrashDbHelper(context); + } + + public long addToTrash(String imagePath) { + LogUtils.d(TAG, "addToTrash: " + imagePath); + File sourceFile = new File(imagePath); + if (!sourceFile.exists()) { + return -1; + } + + String uniqueId = UUID.randomUUID().toString(); + String extension = getExtension(imagePath); + String newFileName = uniqueId + extension; + String trashPath = TrashDbHelper.getTrashPath(); + File destFile = new File(trashPath, newFileName); + + if (sourceFile.renameTo(destFile)) { + String originalFolder = sourceFile.getParent(); + long result = dbHelper.insert(newFileName, imagePath, originalFolder); + LogUtils.i(TAG, "Added to trash: " + newFileName); + return result; + } + LogUtils.e(TAG, "Failed to move to trash"); + return -1; + } + + public Cursor getTrashList() { + return dbHelper.getAll(); + } + + public boolean restore(long id, String fileName, String originalPath) { + LogUtils.i(TAG, "restore: " + fileName + " -> " + originalPath); + File trashFile = new File(TrashDbHelper.getTrashPath(), fileName); + if (!trashFile.exists()) { + return false; + } + + File originalFolder = new File(originalPath).getParentFile(); + if (originalFolder != null && !originalFolder.exists()) { + originalFolder.mkdirs(); + } + + File originalFile = new File(originalPath); + String restoreName = originalFile.getName(); + File restoreFile = new File(originalFolder, restoreName); + + if (trashFile.renameTo(restoreFile)) { + dbHelper.delete(id); + LogUtils.i(TAG, "Restored: " + fileName); + return true; + } + + LogUtils.e(TAG, "Failed to restore: " + fileName); + return false; + } + + public boolean deletePermanently(long id, String fileName) { + File trashFile = new File(TrashDbHelper.getTrashPath(), fileName); + boolean deleted = trashFile.delete(); + if (deleted) { + dbHelper.delete(id); + } + return deleted; + } + + public void clearTrash() { + Cursor cursor = getTrashList(); + if (cursor != null) { + while (cursor.moveToNext()) { + try { + int colIndex = cursor.getColumnIndexOrThrow("_id"); + if (!cursor.isNull(colIndex)) { + String fileName = cursor.getString(cursor.getColumnIndexOrThrow("file_name")); + File trashFile = new File(TrashDbHelper.getTrashPath(), fileName); + trashFile.delete(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + cursor.close(); + } + dbHelper.clear(); + } + + private String getExtension(String path) { + int lastDot = path.lastIndexOf('.'); + if (lastDot > 0) { + return path.substring(lastDot); + } + return ".jpg"; + } +} \ No newline at end of file diff --git a/gallery/src/main/res/drawable-v24/ic_launcher_foreground.xml b/gallery/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/gallery/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/gallery/src/main/res/drawable/ic_back.xml b/gallery/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000..0cdf891 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_back.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_delete.xml b/gallery/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000..aa0f7a0 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_launcher_background.xml b/gallery/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gallery/src/main/res/drawable/ic_restore.xml b/gallery/src/main/res/drawable/ic_restore.xml new file mode 100644 index 0000000..f607ca2 --- /dev/null +++ b/gallery/src/main/res/drawable/ic_restore.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/gallery/src/main/res/drawable/ic_share.xml b/gallery/src/main/res/drawable/ic_share.xml new file mode 100644 index 0000000..ff756ae --- /dev/null +++ b/gallery/src/main/res/drawable/ic_share.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/gallery/src/main/res/layout/activity_image_viewer.xml b/gallery/src/main/res/layout/activity_image_viewer.xml new file mode 100644 index 0000000..3d7ef55 --- /dev/null +++ b/gallery/src/main/res/layout/activity_image_viewer.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/src/main/res/layout/activity_main.xml b/gallery/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..ccb42b7 --- /dev/null +++ b/gallery/src/main/res/layout/activity_main.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/gallery/src/main/res/layout/activity_settings.xml b/gallery/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000..24d2676 --- /dev/null +++ b/gallery/src/main/res/layout/activity_settings.xml @@ -0,0 +1,39 @@ + + + + + + + + + +