Compare commits

..

25 Commits

Author SHA1 Message Date
ZhanGSKen 7f0dc80513 <bytheway>APK 15.20.4 release Publish. 2026-07-10 20:26:27 +08:00
MiMo 798fffe295 feat: 开屏动画支持触摸立即终止
开屏展现期间任意触摸可立即终止动画,进入主窗口。

- 添加OnTouchListener监听触摸事件
- 抽取dismissSplash()统一处理定时到期和触摸终止
- 清除Handler回调和所有子视图动画防止内存泄漏
- 防止重复调用dismissSplash()导致异常
2026-07-10 20:23:28 +08:00
MiMo 1155e8c5c8 feat: 添加主界面开屏动画
MainActivity首次启动时显示5秒开屏画面,展示应用标题和副标题。

- 使用FrameLayout覆盖层实现居中展示
- 标题和副标题依次淡入,5秒后整体淡出
- 开屏背景使用主色调,文字白色/半透明白色
- onDestroy中清理Handler防止内存泄漏
2026-07-10 20:18:03 +08:00
ZhanGSKen 0ca74b324b 锚点位置数据模型 2026-07-10 19:49:20 +08:00
ZhanGSKen bc08da41e5 <bytheway>APK 15.20.3 release Publish. 2026-07-10 13:02:55 +08:00
ZhanGSKen da1c07da56 调整GPS数据接收时间间隔为5秒。 2026-07-10 12:59:34 +08:00
ZhanGSKen 91d97efe4e <bytheway>APK 15.20.2 release Publish. 2026-07-09 20:17:02 +08:00
MiMo 1d4b232491 Merge remote-tracking branch 'origin/bytheway' into merge 2026-07-09 20:11:22 +08:00
MiMo 33f74f8226 fix: 修复GpsReceiveService GPS计数值不更新问题
将GPS接收方式从BroadcastReceiver替换为LocationManager+LocationListener,
解决因无广播发送方导致计数值永远为0的问题。

- 使用LocationManager.requestLocationUpdates主动获取GPS定位
- 新增currentGpsBean内存缓存当前最新GPS数据
- 通知栏显示GPS数据信息(经纬度/精度)和接收次数
- 新增getCurrentGpsBean()供外部获取当前GPS数据
2026-07-09 20:07:38 +08:00
ZhanGSKen a74ab6aa06 更新说明书里的中文应用名说法为之前改过的名称。 2026-07-07 00:47:15 +08:00
MiMo 8b3c6c3481 主窗口菜单添加应用介绍入口,点击打开AboutActivity
- strings.xml新增action_about字符串资源
- activity_main.xml菜单新增action_about菜单项
- MainActivity onOptionsItemSelected添加action_about点击处理
2026-07-07 00:42:53 +08:00
ZhanGSKen 8a50eaf24c 添加应用内公共抽象窗口类,添加应用介绍窗口资源。 2026-07-07 00:34:39 +08:00
MiMo d4fddaecf9 GPS接收服务增加数据接收次数计数并在通知栏实时显示
- onCreate初始化gpsReceiveCount计数器归零
- parseAndSaveGpsData每次接收数据后递增计数并刷新通知栏
- 通知栏文本更新为"已接收GPS数据 N 次"
2026-07-07 00:13:26 +08:00
MiMo 7c8800f873 GpsUtil转型为GPS数据处理模块,封装数据库操作
- GpsUtil新增insertGps、queryAllGps、queryByTime、clearAllGps静态方法
- GpsDbHelper内嵌为GpsUtil私有内部类,外部不可直接访问
- 删除原GpsDbHelper.java和GpsDbManager.java独立文件
- GpsReceiveService和GpsListActivity改为通过GpsUtil访问数据库
2026-07-06 20:23:58 +08:00
MiMo 32afb3ad74 添加GPS数据处理工具类,整合分散的GPS处理方法
- 新增 GpsUtil 工具类,提供 parseFromIntent、parseFromCursor、
  formatTime、formatLatLng、formatOther、speedToKmh、formatSpeed 方法
- GpsReceiveService 调用 GpsUtil.parseFromIntent() 替代内联解析
- GpsAdapter 调用 GpsUtil 格式化方法替代内联格式化,移除 SimpleDateFormat 字段
- GpsDbManager 调用 GpsUtil.parseFromCursor() 消除两处重复的游标映射代码
2026-07-06 20:02:13 +08:00
ZhanGSKen afd57c8d70 更正服务类的属性和方法命名,明确服务分类。 2026-07-06 14:19:58 +08:00
MiMo 8b4624982c feat: GPS定位服务增加运行状态查询接口
- GpsReceiveService 新增公共静态函数 isServiceRunning()
- SettingsActivity 窗口初始化时查询服务状态并同步开关状态
2026-07-06 13:58:47 +08:00
ZhanGSKen 5283ebadc9 GPS信息接收服务总开关UI接口优化。 2026-07-05 15:05:56 +08:00
ZhanGSKen e7fdf3ce6a <bytheway>APK 15.20.1 release Publish. 2026-07-03 13:02:58 +08:00
ZhanGSKen 7407ba582e 更新应用中文名称为更贴切的应用场景名称 2026-07-03 13:00:59 +08:00
ZhanGSKen 6a9112fd20 <bytheway>APK 15.20.0 release Publish. 2026-07-03 12:40:12 +08:00
MiMo 7d960fd445 MainActivity: 主窗口菜单添加GPS列表入口,点击打开GpsListActivity 2026-07-02 16:57:20 +08:00
BigPickle e443e54548 组合框架应用操作流程响应 2026-07-02 16:38:40 +08:00
ZhanGSKen ecb7f9b881 添加GPS数据处理框架 2026-07-02 15:39:23 +08:00
ZhanGSKen 248ffcc5f1 创建ByTheWay项目主体框架 2026-07-02 14:10:15 +08:00
136 changed files with 1797 additions and 5615 deletions
+4 -4
View File
@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sat Jul 11 20:07:00 HKT 2026
stageCount=19
#Mon Jun 29 11:28:11 HKT 2026
stageCount=18
libraryProject=libaes
baseVersion=15.20
publishVersion=15.20.18
publishVersion=15.20.17
buildCount=0
baseBetaVersion=15.20.19
baseBetaVersion=15.20.18
+1 -1
View File
@@ -5,7 +5,7 @@
</style>
<style name="MyDebugActivityTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:statusBarColor">?attr/colorPrimaryDark</item>
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
<item name="colorTittle">@color/mainWindowTextColor</item>
<item name="colorTittleBackgound">@color/toolbarBackgroundColor</item>
<item name="colorText">@color/debugTextColor</item>
+1 -1
View File
@@ -5,7 +5,7 @@
</style>
<style name="MyDebugActivityTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:statusBarColor">?attr/colorPrimaryDark</item>
<item name="android:statusBarColor">@color/toolbarBackgroundColor</item>
<item name="colorTittle">@color/mainWindowTextColor</item>
<item name="colorTittleBackgound">@color/toolbarBackgroundColor</item>
<item name="colorText">@color/debugTextColor</item>
+1
View File
@@ -0,0 +1 @@
/build
+9 -4
View File
@@ -1,7 +1,11 @@
# Gallery
# [ByTheWay]
#【顺道说】
#### 介绍
云宝相册应用
The [ByTheWay] project is designed to quickly schedule task reminders related to time and location.
Its main feature is the ability to quickly create task-related data.
【顺道说】项目应用应用于快速安排一个以时间和位置有关的任务提醒事项。
其主要的特点是能快速建立事项任务数据。
#### 软件架构
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
@@ -10,15 +14,16 @@
#### Gradle 编译说明
调试版编译命令 gradle assembleBetaDebug
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh gallery
阶段版编译命令 bash .winboll/bashPublishAPKAddTag.sh bytheway
#### 使用说明
在安卓操作系统里使用。
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码 : ZhanGSKen(ZhanGSKen<zhangsken@188.com>)
3. 提交代码 : ZhanGSKen(ZhanGSKen<ZhanGSKen@QQ.COM>)
4. 新建 Pull Request
+20 -29
View File
@@ -23,34 +23,25 @@ android {
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "cc.winboll.studio.gallery"
minSdkVersion 26
// MIUI12
applicationId "cc.winboll.studio.bytheway"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
//1. Android
//-  versionCode  int
//- Java int 2147483647
versionCode 1520000
// versionName
// .winboll/winbollBuildProps.properties stageCount=0
// build.gradle stageCount=0
// Gradle编译环境下合起来的 versionName "${versionName}.0"
versionName "15.0"
versionName "15.20"
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'
//
@@ -67,8 +58,6 @@ dependencies {
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'
@@ -82,11 +71,10 @@ dependencies {
//
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//使sdk
//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'
@@ -103,17 +91,20 @@ dependencies {
implementation "io.noties.markwon:linkify:$markwonVersion"
implementation "io.noties.markwon:recycler:$markwonVersion"
*/
/*implementation 'com.termux:terminal-emulator:0.118.0'
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.20.21'
api 'cc.winboll.studio:libappbase:15.20.34'
// Biometric ()
implementation 'androidx.biometric:biometric:1.1.0'
// WinBoLL备用 jitpack.io
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
// WinBoLL库 nexus.winboll.cc
api 'cc.winboll.studio:libappbase:15.20.34'
api 'cc.winboll.studio:libaes:15.20.17'
// jitpack.io
//api 'com.github.ZhanGSKen:libappbase:appbase-v15.20.33'
//api 'com.github.ZhanGSKen:libaes:aes-v15.20.16'
api fileTree(dir: 'libs', include: ['*.jar'])
}
+8
View File
@@ -0,0 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Fri Jul 10 20:26:27 HKT 2026
stageCount=5
libraryProject=
baseVersion=15.20
publishVersion=15.20.4
buildCount=0
baseBetaVersion=15.20.5
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">顺道说☯</string>string>
</resources>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ByTheWay +</string>
</resources>
+60
View File
@@ -0,0 +1,60 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.bytheway">
<!-- 只能在前台获取精确的位置信息 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- 只有在前台运行时才能获取大致位置信息 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- 在后台使用位置信息 -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<!-- 运行前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/MyAppTheme"
android:resizeableActivity="true"
android:name=".App">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity
android:name=".activities.GpsListActivity"
android:exported="true"/>
<service
android:name=".services.GpsReceiveService"
android:launchMode="singleInstance"
android:exported="false"/>
<activity android:name="cc.winboll.studio.bytheway.activities.SettingsActivity"/>
<activity android:name="cc.winboll.studio.bytheway.activities.AboutActivity"/>
</application>
</manifest>
@@ -0,0 +1,359 @@
package cc.winboll.studio.bytheway;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.Thread.UncaughtExceptionHandler;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public class App extends GlobalApplication {
private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
@Override
public void onCreate() {
try {
super.onCreate();
WinBoLLActivityManager.init(this);
// 初始化主题样式ID列表
AESThemeUtil.init(null);
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
ToastUtils.init(getApplicationContext());
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashActivity.class
);
}
}
public static void write(InputStream input, OutputStream output) throws IOException {
byte[] buf = new byte[1024 * 8];
int len;
while ((len = input.read(buf)) != -1) {
output.write(buf, 0, len);
}
}
public static void write(File file, byte[] data) throws IOException {
File parent = file.getParentFile();
if (parent != null && !parent.exists()) parent.mkdirs();
ByteArrayInputStream input = new ByteArrayInputStream(data);
FileOutputStream output = new FileOutputStream(file);
try {
write(input, output);
} finally {
closeIO(input, output);
}
}
public static String toString(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
write(input, output);
try {
return output.toString("UTF-8");
} finally {
closeIO(input, output);
}
}
public static void closeIO(Closeable... closeables) {
for (Closeable closeable : closeables) {
try {
if (closeable != null) closeable.close();
} catch (IOException ignored) {}
}
}
public static class CrashHandler {
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
private static CrashHandler sInstance;
private PartCrashHandler mPartCrashHandler;
public static CrashHandler getInstance() {
if (sInstance == null) {
sInstance = new CrashHandler();
}
return sInstance;
}
public void registerGlobal(Context context) {
registerGlobal(context, null);
}
public void registerGlobal(Context context, String crashDir) {
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerImpl(context.getApplicationContext(), crashDir));
}
public void unregister() {
Thread.setDefaultUncaughtExceptionHandler(DEFAULT_UNCAUGHT_EXCEPTION_HANDLER);
}
public void registerPart(Context context) {
unregisterPart(context);
mPartCrashHandler = new PartCrashHandler(context.getApplicationContext());
MAIN_HANDLER.postAtFrontOfQueue(mPartCrashHandler);
}
public void unregisterPart(Context context) {
if (mPartCrashHandler != null) {
mPartCrashHandler.isRunning.set(false);
mPartCrashHandler = null;
}
}
private static class PartCrashHandler implements Runnable {
private final Context mContext;
public AtomicBoolean isRunning = new AtomicBoolean(true);
public PartCrashHandler(Context context) {
this.mContext = context;
}
@Override
public void run() {
while (isRunning.get()) {
try {
Looper.loop();
} catch (final Throwable e) {
e.printStackTrace();
if (isRunning.get()) {
MAIN_HANDLER.post(new Runnable(){
@Override
public void run() {
Toast.makeText(mContext, e.toString(), Toast.LENGTH_LONG).show();
}
});
} else {
if (e instanceof RuntimeException) {
throw (RuntimeException)e;
} else {
throw new RuntimeException(e);
}
}
}
}
}
}
private static class UncaughtExceptionHandlerImpl implements UncaughtExceptionHandler {
private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
private final Context mContext;
private final File mCrashDir;
public UncaughtExceptionHandlerImpl(Context context, String crashDir) {
this.mContext = context;
this.mCrashDir = TextUtils.isEmpty(crashDir) ? new File(mContext.getExternalCacheDir(), "crash") : new File(crashDir);
}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
try {
String log = buildLog(throwable);
writeLog(log);
try {
Intent intent = new Intent(mContext, CrashActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_TEXT, log);
mContext.startActivity(intent);
} catch (Throwable e) {
e.printStackTrace();
writeLog(e.toString());
}
throwable.printStackTrace();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
} catch (Throwable e) {
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
}
}
private String buildLog(Throwable throwable) {
String time = DATE_FORMAT.format(new Date());
String versionName = "unknown";
long versionCode = 0;
try {
PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
versionName = packageInfo.versionName;
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
} catch (Throwable ignored) {}
LinkedHashMap<String, String> head = new LinkedHashMap<String, String>();
head.put("Time Of Crash", time);
head.put("Device", String.format("%s, %s", Build.MANUFACTURER, Build.MODEL));
head.put("Android Version", String.format("%s (%d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT));
head.put("App Version", String.format("%s (%d)", versionName, versionCode));
head.put("Kernel", getKernel());
head.put("Support Abis", Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS != null ? Arrays.toString(Build.SUPPORTED_ABIS): "unknown");
head.put("Fingerprint", Build.FINGERPRINT);
StringBuilder builder = new StringBuilder();
for (String key : head.keySet()) {
if (builder.length() != 0) builder.append("\n");
builder.append(key);
builder.append(" : ");
builder.append(head.get(key));
}
builder.append("\n\n");
builder.append(Log.getStackTraceString(throwable));
return builder.toString();
}
private void writeLog(String log) {
String time = DATE_FORMAT.format(new Date());
File file = new File(mCrashDir, "crash_" + time + ".txt");
try {
write(file, log.getBytes("UTF-8"));
} catch (Throwable e) {
e.printStackTrace();
}
}
private static String getKernel() {
try {
return App.toString(new FileInputStream("/proc/version")).trim();
} catch (Throwable e) {
return e.getMessage();
}
}
}
}
public static final class CrashActivity extends Activity {
private String mLog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(android.R.style.Theme_DeviceDefault);
setTitle("App Crash");
mLog = getIntent().getStringExtra(Intent.EXTRA_TEXT);
ScrollView contentView = new ScrollView(this);
contentView.setFillViewport(true);
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(this);
TextView textView = new TextView(this);
int padding = dp2px(16);
textView.setPadding(padding, padding, padding, padding);
textView.setText(mLog);
textView.setTextIsSelectable(true);
textView.setTypeface(Typeface.DEFAULT);
textView.setLinksClickable(true);
horizontalScrollView.addView(textView);
contentView.addView(horizontalScrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
setContentView(contentView);
}
private void restart() {
Intent intent = getPackageManager().getLaunchIntentForPackage(getPackageName());
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
private static int dp2px(float dpValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, android.R.id.copy, 0, android.R.string.copy)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.copy:
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
restart();
}
}
}
@@ -0,0 +1,140 @@
package cc.winboll.studio.bytheway;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.TextView;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.bytheway.activities.AboutActivity;
import cc.winboll.studio.bytheway.activities.BaseWinBoLLActivity;
import cc.winboll.studio.bytheway.activities.GpsListActivity;
import cc.winboll.studio.bytheway.activities.SettingsActivity;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.DevelopUtils;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
public class MainActivity extends BaseWinBoLLActivity {
public static final String TAG = "MainActivity";
private static final long SPLASH_DURATION = 5000L;
private static final long SPLASH_FADE_IN = 800L;
private static final long SPLASH_FADE_OUT = 500L;
private View splashOverlay;
private final Handler splashHandler = new Handler();
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
//AESThemeUtil.applyAppCompatTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
initSplash();
ToastUtils.show("onCreate");
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onDestroy() {
splashHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
AESThemeUtil.inflateCompatThemeMenu(this, menu);
if (GlobalApplication.isDebugging()) {
DevelopUtils.inflateMenu(this, menu);
}
getMenuInflater().inflate(cc.winboll.studio.libaes.R.menu.toolbar_drawerbase, menu);
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (AESThemeUtil.onWinBoLLThemeItemSelected(this, item)) {
recreate();
}
if (item.getItemId() == R.id.action_gps_list) {
startActivity(new Intent(this, GpsListActivity.class));
return true;
}
if (item.getItemId() == R.id.action_settings) {
startActivity(new Intent(this, SettingsActivity.class));
return true;
}
if (item.getItemId() == R.id.action_about) {
startActivity(new Intent(this, AboutActivity.class));
return true;
}
if (DevelopUtils.onDevelopItemSelected(this, item)) {
LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId()));
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
private void initSplash() {
splashOverlay = findViewById(R.id.splash_overlay);
final TextView splashTitle = findViewById(R.id.splash_title);
final TextView splashSubtitle = findViewById(R.id.splash_subtitle);
splashOverlay.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, android.view.MotionEvent event) {
dismissSplash();
return true;
}
});
AlphaAnimation fadeInTitle = new AlphaAnimation(0f, 1f);
fadeInTitle.setDuration(SPLASH_FADE_IN);
fadeInTitle.setFillAfter(true);
splashTitle.startAnimation(fadeInTitle);
AlphaAnimation fadeInSubtitle = new AlphaAnimation(0f, 1f);
fadeInSubtitle.setDuration(SPLASH_FADE_IN);
fadeInSubtitle.setStartOffset(SPLASH_FADE_IN);
fadeInSubtitle.setFillAfter(true);
splashSubtitle.startAnimation(fadeInSubtitle);
splashHandler.postDelayed(new Runnable() {
@Override
public void run() {
dismissSplash();
}
}, SPLASH_DURATION);
}
private void dismissSplash() {
splashHandler.removeCallbacksAndMessages(null);
if (splashOverlay == null || splashOverlay.getVisibility() != View.VISIBLE) {
return;
}
splashOverlay.clearAnimation();
for (int i = 0; i < ((android.view.ViewGroup) splashOverlay).getChildCount(); i++) {
((android.view.ViewGroup) splashOverlay).getChildAt(i).clearAnimation();
}
splashOverlay.setVisibility(View.GONE);
}
}
@@ -0,0 +1,79 @@
package cc.winboll.studio.bytheway.activities;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.bytheway.MainActivity;
import cc.winboll.studio.bytheway.R;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.APPInfo;
import cc.winboll.studio.libappbase.views.AboutView;
/**
* @Author 豆包&BigPickle&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/07 00:17
* @Describe 应用介绍窗口
*/
public class AboutActivity extends BaseWinBoLLActivity {
public static final String TAG = "AboutActivity";
private Toolbar mToolbar;
@Override
public String getTag() {
return TAG;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
// 设置工具栏
initToolbar();
AboutView aboutView = findViewById(R.id.aboutview);
aboutView.setAPPInfo(genDefaultAppInfo());
}
private void initToolbar() {
LogUtils.d(TAG, "initToolbar() 开始初始化");
mToolbar = findViewById(R.id.toolbar);
if (mToolbar == null) {
LogUtils.e(TAG, "initToolbar() | Toolbar未找到");
return;
}
setSupportActionBar(mToolbar);
mToolbar.setSubtitle(getTag());
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "导航栏 点击返回按钮");
WinBoLLActivityManager.getInstance().resumeActivity(MainActivity.class);
WinBoLLActivityManager.getInstance().finish(AboutActivity.this);
}
});
LogUtils.d(TAG, "initToolbar() 配置完成");
}
private APPInfo genDefaultAppInfo() {
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
String branchName = "bytheway";
APPInfo appInfo = new APPInfo();
appInfo.setAppName(getString(R.string.app_name));
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("MyWinBoLL");
appInfo.setAppGitOwner("ZhanGSKen");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=ByTheWay");
appInfo.setAppAPKName("ByTheWay");
appInfo.setAppAPKFolderName("ByTheWay");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo;
}
}
@@ -0,0 +1,46 @@
package cc.winboll.studio.bytheway.activities;
import android.app.Activity;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
import cc.winboll.studio.libaes.models.AESThemeBean;
import cc.winboll.studio.libaes.utils.AESThemeUtil;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
/**
* @Author 豆包&BigPickle&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/07 00:21
* @Describe BaseWinBollActivity 【继承AppCompatActivity,保留核心能力,不额外暴露方法】
* 继承链路:BaseWinBoLLActivity → AppCompatActivity → FragmentActivityAppCompat能力天然继承可用
*/
public abstract class BaseWinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
public static final String TAG = "BaseWinBoLLActivity";
protected volatile AESThemeBean.ThemeType mThemeType;
@Override
protected void onCreate(Bundle savedInstanceState) {
//AESThemeUtil.applyAppCompatTheme(this);
mThemeType = AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
super.onCreate(savedInstanceState);
WinBoLLActivityManager.getInstance().add(this);
}
@Override
protected void onDestroy() {
WinBoLLActivityManager.getInstance().registeRemove(this);
super.onDestroy();
}
// 子类必须实现getTag(),确保唯一标识
@Override
public abstract String getTag();
@Override
public Activity getActivity() {
return this;
}
}
@@ -0,0 +1,76 @@
package cc.winboll.studio.bytheway.activities;
import android.os.Bundle;
import android.os.Handler;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import cc.winboll.studio.bytheway.R;
import cc.winboll.studio.bytheway.adapters.GpsAdapter;
import cc.winboll.studio.bytheway.models.GpsBean;
import cc.winboll.studio.bytheway.utils.GpsUtil;
import cc.winboll.studio.libappbase.LogUtils;
import java.util.List;
/**
* GPS定位数据列表展示页面
* 支持下拉刷新SQLite本地数据库定位轨迹信息
* @author 豆包&ZhanGSKen<zhangsken@qq.com>
* 创建时间:2026-07-02 14:49:00
* 最后编辑时间:2026-07-02 16:32:13
*/
public class GpsListActivity extends AppCompatActivity {
private SwipeRefreshLayout swipeRefresh;
private RecyclerView rvGps;
private GpsAdapter adapter;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_gps_list);
LogUtils.i("GpsListActivity", "页面初始化");
initView();
initData();
setSwipeRefreshListener();
}
private void initView() {
swipeRefresh = findViewById(R.id.swipe_refresh_layout);
rvGps = findViewById(R.id.rv_gps_list);
rvGps.setLayoutManager(new LinearLayoutManager(this));
LogUtils.i("GpsListActivity", "控件初始化完成");
}
private void initData() {
LogUtils.i("GpsListActivity", "开始加载GPS数据库数据");
refreshGpsData();
}
private void setSwipeRefreshListener() {
swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
refreshGpsData();
}
});
}
private void refreshGpsData() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
final List<GpsBean> gpsDataList = GpsUtil.queryAllGps(GpsListActivity.this);
LogUtils.i("GpsListActivity", "查询GPS数据条数:" + gpsDataList.size());
adapter = new GpsAdapter(GpsListActivity.this, gpsDataList);
rvGps.setAdapter(adapter);
swipeRefresh.setRefreshing(false);
}
}, 500);
}
}
@@ -0,0 +1,33 @@
package cc.winboll.studio.bytheway.activities;
import android.app.Activity;
import android.os.Bundle;
import android.widget.CompoundButton;
import android.widget.Switch;
import cc.winboll.studio.bytheway.R;
import cc.winboll.studio.bytheway.services.GpsReceiveService;
public class SettingsActivity extends Activity {
public static final String TAG = "SettingsActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
Switch swGpsReceiveService = findViewById(R.id.sw_gps_receive_service);
swGpsReceiveService.setChecked(GpsReceiveService.isServiceRunning());
swGpsReceiveService.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
GpsReceiveService.startGpsReceiveService(SettingsActivity.this);
} else {
GpsReceiveService.stopGpsReceiveService(SettingsActivity.this);
}
}
});
}
}
@@ -0,0 +1,64 @@
package cc.winboll.studio.bytheway.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import cc.winboll.studio.bytheway.R;
import cc.winboll.studio.bytheway.models.GpsBean;
import cc.winboll.studio.bytheway.utils.GpsUtil;
import java.util.List;
/**
* @Author 豆包&BigPickle&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/02 15:10
*/
public class GpsAdapter extends RecyclerView.Adapter<GpsAdapter.GpsHolder> {
private final Context context;
private final List<GpsBean> gpsList;
public GpsAdapter(Context context, List<GpsBean> gpsList) {
this.context = context;
this.gpsList = gpsList;
}
@NonNull
@Override
public GpsHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.item_gps, parent, false);
return new GpsHolder(view);
}
@Override
public void onBindViewHolder(@NonNull GpsHolder holder, int position) {
GpsBean bean = gpsList.get(position);
String time = GpsUtil.formatTime(bean.getReceiveTime());
holder.tvTime.setText("接收时间:" + time);
holder.tvLatLng.setText(GpsUtil.formatLatLng(bean.getLatitude(), bean.getLongitude()));
holder.tvOther.setText(GpsUtil.formatOther(bean.getAccuracy(), bean.getAltitude(), bean.getSpeed()));
}
@Override
public int getItemCount() {
return gpsList.size();
}
public static class GpsHolder extends RecyclerView.ViewHolder {
TextView tvTime, tvLatLng, tvOther;
public GpsHolder(@NonNull View itemView) {
super(itemView);
tvTime = itemView.findViewById(R.id.tv_time);
tvLatLng = itemView.findViewById(R.id.tv_lat_lng);
tvOther = itemView.findViewById(R.id.tv_other);
}
}
}
@@ -0,0 +1,80 @@
package cc.winboll.studio.bytheway.models;
/**
* @Author 豆包&BigPickle&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/10 19:46
* @Describe 锚点位置数据模型
* 存储轨迹标记锚点:经纬度、排序序号、自定义备注信息
* @author 豆包&ZhanGSKen<zhangsken@qq.com>
* 创建时间:2026-07-10 13:45:00
* 最后编辑时间:2026-07-10 13:48:00
*/
public class AnchorPositionBean {
public static final String TAG = "AnchorPositionBean";
// 数据库自增主键ID
private int id;
// 锚点纬度
private double latitude;
// 锚点经度
private double longitude;
// 锚点排序序号,用于轨迹先后展示
private int sortIndex;
// 锚点备注文字
private String remark;
// 无参构造(SQLite反射实例化必需)
public AnchorPositionBean() {
}
// 全参构造
public AnchorPositionBean(final double latitude, final double longitude, final int sortIndex, final String remark) {
this.latitude = latitude;
this.longitude = longitude;
this.sortIndex = sortIndex;
this.remark = remark;
}
public int getId() {
return id;
}
public void setId(final int id) {
this.id = id;
}
public double getLatitude() {
return latitude;
}
public void setLatitude(final double latitude) {
this.latitude = latitude;
}
public double getLongitude() {
return longitude;
}
public void setLongitude(final double longitude) {
this.longitude = longitude;
}
public int getSortIndex() {
return sortIndex;
}
public void setSortIndex(final int sortIndex) {
this.sortIndex = sortIndex;
}
public String getRemark() {
return remark;
}
public void setRemark(final String remark) {
this.remark = remark;
}
}
@@ -0,0 +1,44 @@
package cc.winboll.studio.bytheway.models;
import java.util.Date;
/**
* @Author 豆包&BigPickle&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/02 14:56
*/
public class GpsBean {
private int id;
private double latitude; //纬度
private double longitude; //经度
private float accuracy; //精度
private double altitude; //海拔
private float speed; //速度
private long receiveTime; //接收时间戳
public GpsBean() {}
public GpsBean(double latitude, double longitude, float accuracy, double altitude, float speed, long receiveTime) {
this.latitude = latitude;
this.longitude = longitude;
this.accuracy = accuracy;
this.altitude = altitude;
this.speed = speed;
this.receiveTime = receiveTime;
}
//get set 方法
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public double getLatitude() { return latitude; }
public void setLatitude(double latitude) { this.latitude = latitude; }
public double getLongitude() { return longitude; }
public void setLongitude(double longitude) { this.longitude = longitude; }
public float getAccuracy() { return accuracy; }
public void setAccuracy(float accuracy) { this.accuracy = accuracy; }
public double getAltitude() { return altitude; }
public void setAltitude(double altitude) { this.altitude = altitude; }
public float getSpeed() { return speed; }
public void setSpeed(float speed) { this.speed = speed; }
public long getReceiveTime() { return receiveTime; }
public void setReceiveTime(long receiveTime) { this.receiveTime = receiveTime; }
}
@@ -0,0 +1,224 @@
package cc.winboll.studio.bytheway.services;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.widget.Toast;
import cc.winboll.studio.bytheway.models.GpsBean;
import cc.winboll.studio.bytheway.utils.GpsUtil;
import cc.winboll.studio.libappbase.LogUtils;
/**
* @Author 豆包&BigPickle&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/02 15:33
* @Describe GPS定位接收服务,通过LocationManager获取GPS数据并持久化存入SQLite数据库。单例独占模式,自带重复启动拦截,对外静态统一启停接口
*/
public class GpsReceiveService extends Service {
public static final String TAG = "GpsReceiveService";
private static final int NOTIFICATION_ID = 1001;
private static final String CHANNEL_ID = "gps_receive_service_channel";
private static boolean serviceRunning = false;
private static GpsReceiveService instance;
private int gpsReceiveCount = 0;
private GpsBean currentGpsBean;
private LocationManager locationManager;
private LocationListener locationListener;
/**
* 获取GPS定位服务运行状态
* @return true=服务正在运行, false=服务未运行
*/
public static boolean isServiceRunning() {
return serviceRunning;
}
/**
* 获取当前最新的GPS定位数据
* @return 当前GPS数据,未收到定位时返回null
*/
public static GpsBean getCurrentGpsBean() {
return serviceRunning ? instance.currentGpsBean : null;
}
/**
* 外部统一静态启动服务
*/
public static void startGpsReceiveService(final Context context) {
if (serviceRunning) {
LogUtils.i("GpsReceiveService", "服务已运行,无需重复启动");
return;
}
final Intent intent = new Intent(context, GpsReceiveService.class);
context.startService(intent);
LogUtils.i("GpsReceiveService", "GPS定位消息接收服务启动请求发送");
}
/**
* 外部统一静态关闭服务
*/
public static void stopGpsReceiveService(final Context context) {
if (!serviceRunning) {
LogUtils.i("GpsReceiveService", "服务未运行,无需关闭");
return;
}
final Intent intent = new Intent(context, GpsReceiveService.class);
context.stopService(intent);
LogUtils.i("GpsReceiveService", "GPS定位消息接收服务停止请求发送");
}
@Override
public void onCreate() {
super.onCreate();
instance = this;
serviceRunning = true;
gpsReceiveCount = 0;
currentGpsBean = null;
startGpsLocationUpdates();
startForeground(NOTIFICATION_ID, createNotification());
LogUtils.i("GpsReceiveService", "GPS定位消息接收服务创建完成");
Toast.makeText(this, "GPS定位消息接收服务已开启", Toast.LENGTH_SHORT).show();
}
private Notification createNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"GPS定位消息接收服务",
NotificationManager.IMPORTANCE_LOW
);
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.createNotificationChannel(channel);
}
String contentText;
if (currentGpsBean != null) {
contentText = "已接收 " + gpsReceiveCount + " 次 | "
+ "纬度:" + currentGpsBean.getLatitude()
+ " 经度:" + currentGpsBean.getLongitude()
+ " 精度:" + currentGpsBean.getAccuracy() + "m";
} else {
contentText = "等待GPS定位数据...";
}
return new Notification.Builder(this, CHANNEL_ID)
.setContentTitle("ByTheWay")
.setContentText(contentText)
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
.setOngoing(true)
.build();
}
/**
* 更新前台服务通知栏显示
*/
private void updateNotification() {
final NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.notify(NOTIFICATION_ID, createNotification());
}
/**
* 启动GPS定位更新监听
*/
private void startGpsLocationUpdates() {
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
locationListener = new LocationListener() {
@Override
public void onLocationChanged(final Location location) {
handleGpsLocation(location);
}
@Override
public void onStatusChanged(final String provider, final int status, final Bundle extras) {
LogUtils.d("GpsReceiveService", "GPS状态变化: " + provider + ", status=" + status);
}
@Override
public void onProviderEnabled(final String provider) {
LogUtils.d("GpsReceiveService", "GPS提供者已启用: " + provider);
}
@Override
public void onProviderDisabled(final String provider) {
LogUtils.d("GpsReceiveService", "GPS提供者已禁用: " + provider);
}
};
try {
// 最小时间间隔1秒,最小距离间隔0米,确保每次位置更新都能接收
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
5000L,
0f,
locationListener
);
LogUtils.i("GpsReceiveService", "GPS定位监听已启动");
} catch (SecurityException e) {
LogUtils.e("GpsReceiveService", "缺少GPS定位权限: " + e.getMessage());
} catch (Exception e) {
LogUtils.e("GpsReceiveService", "启动GPS定位监听失败: " + e.getMessage());
}
}
/**
* 处理GPS定位数据,写入SQLite数据库并更新通知栏
*/
private void handleGpsLocation(final Location location) {
final GpsBean bean = new GpsBean();
bean.setLatitude(location.getLatitude());
bean.setLongitude(location.getLongitude());
bean.setAccuracy(location.getAccuracy());
bean.setAltitude(location.getAltitude());
bean.setSpeed(location.getSpeed());
bean.setReceiveTime(System.currentTimeMillis());
currentGpsBean = bean;
GpsUtil.insertGps(getApplicationContext(), bean);
gpsReceiveCount++;
updateNotification();
LogUtils.d("GpsReceiveService", "收到GPS定位,已保存数据,累计接收 " + gpsReceiveCount + "");
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
//单例不重启模式,被杀不自动重启
return START_NOT_STICKY;
}
@Override
public IBinder onBind(final Intent intent) {
return null;
}
@Override
public void onDestroy() {
stopGpsLocationUpdates();
stopForeground(true);
serviceRunning = false;
instance = null;
LogUtils.i("GpsReceiveService", "GPS接收服务已销毁,定位监听已停止");
Toast.makeText(this, "GPS定位服务已关闭", Toast.LENGTH_SHORT).show();
super.onDestroy();
}
/**
* 停止GPS定位更新监听
*/
private void stopGpsLocationUpdates() {
if (locationManager != null && locationListener != null) {
try {
locationManager.removeUpdates(locationListener);
LogUtils.i("GpsReceiveService", "GPS定位监听已移除");
} catch (SecurityException e) {
LogUtils.e("GpsReceiveService", "移除GPS监听异常: " + e.getMessage());
}
}
}
}
@@ -0,0 +1,247 @@
package cc.winboll.studio.bytheway.utils;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import cc.winboll.studio.bytheway.models.GpsBean;
import cc.winboll.studio.libappbase.LogUtils;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/**
* GPS数据处理模块
* 集合GPS数据解析、格式化、数据库存取等所有操作,外部统一通过本类访问
* @author ZhanGSKen<zhangsken@qq.com>
*/
public class GpsUtil {
private static final String TAG = "GpsUtil";
// Intent广播中的GPS数据键名
public static final String KEY_LATITUDE = "latitude";
public static final String KEY_LONGITUDE = "longitude";
public static final String KEY_ACCURACY = "accuracy";
public static final String KEY_ALTITUDE = "altitude";
public static final String KEY_SPEED = "speed";
public static final String KEY_RECEIVE_TIME = "receive_time";
// 时间格式
private static final String FORMAT_TIME = "yyyy-MM-dd HH:mm:ss";
// ======================== 数据解析方法 ========================
/**
* 从Intent广播数据中解析GPS定位信息,构造GpsBean对象
* @param intent GPS定位广播Intent
* @return 解析后的GpsBean对象
*/
public static GpsBean parseFromIntent(final Intent intent) {
final double latitude = intent.getDoubleExtra(KEY_LATITUDE, 0);
final double longitude = intent.getDoubleExtra(KEY_LONGITUDE, 0);
final float accuracy = intent.getFloatExtra(KEY_ACCURACY, 0);
final double altitude = intent.getDoubleExtra(KEY_ALTITUDE, 0);
final float speed = intent.getFloatExtra(KEY_SPEED, 0);
final long time = intent.getLongExtra(KEY_RECEIVE_TIME, System.currentTimeMillis());
final GpsBean bean = new GpsBean();
bean.setLatitude(latitude);
bean.setLongitude(longitude);
bean.setAccuracy(accuracy);
bean.setAltitude(altitude);
bean.setSpeed(speed);
bean.setReceiveTime(time);
return bean;
}
/**
* 从数据库游标中解析GPS定位信息,构造GpsBean对象
* 游标字段顺序:id, latitude, longitude, accuracy, altitude, speed, receive_time
* @param cursor 数据库查询游标(需已定位到目标行)
* @return 解析后的GpsBean对象
*/
public static GpsBean parseFromCursor(final Cursor cursor) {
final GpsBean bean = new GpsBean();
bean.setId(cursor.getInt(0));
bean.setLatitude(cursor.getDouble(1));
bean.setLongitude(cursor.getDouble(2));
bean.setAccuracy(cursor.getFloat(3));
bean.setAltitude(cursor.getDouble(4));
bean.setSpeed(cursor.getFloat(5));
bean.setReceiveTime(cursor.getLong(6));
return bean;
}
// ======================== 格式化方法 ========================
/**
* 将时间戳格式化为可读时间字符串
* @param timestamp 毫秒时间戳
* @return 格式化后的时间字符串
*/
public static String formatTime(final long timestamp) {
final SimpleDateFormat sdf = new SimpleDateFormat(FORMAT_TIME, Locale.getDefault());
return sdf.format(new Date(timestamp));
}
/**
* 格式化经纬度显示文本
* @param latitude 纬度
* @param longitude 经度
* @return 格式化后的经纬度字符串
*/
public static String formatLatLng(final double latitude, final double longitude) {
return "纬度:" + latitude + " 经度:" + longitude;
}
/**
* 格式化精度、海拔、速度显示文本
* @param accuracy 精度(米)
* @param altitude 海拔(米)
* @param speed 速度(m/s
* @return 格式化后的字符串
*/
public static String formatOther(final float accuracy, final double altitude, final float speed) {
return "精度:" + accuracy + "m 海拔:" + altitude + "m 速度:" + speed + "m/s";
}
/**
* 将速度从m/s转换为km/h
* @param speedMps 速度(m/s
* @return 速度(km/h
*/
public static float speedToKmh(final float speedMps) {
return speedMps * 3.6f;
}
/**
* 格式化速度显示文本(km/h)
* @param speedMps 速度(m/s
* @return 格式化后的速度字符串
*/
public static String formatSpeed(final float speedMps) {
return speedToKmh(speedMps) + "km/h";
}
// ======================== 数据库操作方法 ========================
/**
* 插入单条GPS定位数据
* @param context 上下文
* @param bean GPS数据对象
*/
public static void insertGps(final Context context, final GpsBean bean) {
final GpsDbHelper helper = new GpsDbHelper(context);
final SQLiteDatabase db = helper.getWritableDatabase();
final ContentValues values = new ContentValues();
values.put("latitude", bean.getLatitude());
values.put("longitude", bean.getLongitude());
values.put("accuracy", bean.getAccuracy());
values.put("altitude", bean.getAltitude());
values.put("speed", bean.getSpeed());
values.put("receive_time", bean.getReceiveTime());
db.insert(GpsDbHelper.TABLE_GPS, null, values);
LogUtils.i(TAG, "GPS数据插入成功");
db.close();
}
/**
* 查询全部GPS记录,按时间倒序排列
* @param context 上下文
* @return GPS数据列表
*/
public static List<GpsBean> queryAllGps(final Context context) {
final List<GpsBean> list = new ArrayList<GpsBean>();
final GpsDbHelper helper = new GpsDbHelper(context);
final SQLiteDatabase db = helper.getReadableDatabase();
final Cursor cursor = db.query(GpsDbHelper.TABLE_GPS, null, null, null, null, null, "receive_time DESC");
if (cursor.moveToFirst()) {
do {
list.add(parseFromCursor(cursor));
} while (cursor.moveToNext());
}
LogUtils.i(TAG, "查询全部GPS条数:" + list.size());
cursor.close();
db.close();
return list;
}
/**
* 根据时间区间查询GPS轨迹
* @param context 上下文
* @param startTime 起始时间戳
* @param endTime 结束时间戳
* @return GPS数据列表
*/
public static List<GpsBean> queryByTime(final Context context, final long startTime, final long endTime) {
final List<GpsBean> list = new ArrayList<GpsBean>();
final GpsDbHelper helper = new GpsDbHelper(context);
final SQLiteDatabase db = helper.getReadableDatabase();
final String where = "receive_time >= ? AND receive_time <= ?";
final String[] args = {String.valueOf(startTime), String.valueOf(endTime)};
final Cursor cursor = db.query(GpsDbHelper.TABLE_GPS, null, where, args, null, null, "receive_time ASC");
if (cursor.moveToFirst()) {
do {
list.add(parseFromCursor(cursor));
} while (cursor.moveToNext());
}
LogUtils.i(TAG, "时间段查询GPS条数:" + list.size());
cursor.close();
db.close();
return list;
}
/**
* 清空所有本地GPS历史数据
* @param context 上下文
*/
public static void clearAllGps(final Context context) {
final GpsDbHelper helper = new GpsDbHelper(context);
final SQLiteDatabase db = helper.getWritableDatabase();
db.delete(GpsDbHelper.TABLE_GPS, null, null);
LogUtils.i(TAG, "已清空全部GPS数据库数据");
db.close();
}
// ======================== 内部类:数据库管理 ========================
/**
* SQLite数据库创建、版本升级管理
*/
private static class GpsDbHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "GpsLocation.db";
private static final int DB_VERSION = 1;
private static final String TABLE_GPS = "gps_table";
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_GPS + " ("
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "latitude REAL,"
+ "longitude REAL,"
+ "accuracy REAL,"
+ "altitude REAL,"
+ "speed REAL,"
+ "receive_time INTEGER"
+ ")";
public GpsDbHelper(final Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(final SQLiteDatabase db) {
db.execSQL(CREATE_TABLE);
}
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_GPS);
onCreate(db);
}
}
}
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" android:background="@drawable/bg_container_border">
<cc.winboll.studio.libaes.views.ASupportToolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/toolbar"/>
<cc.winboll.studio.libappbase.views.AboutView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:id="@+id/aboutview"/>
</LinearLayout>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_gps_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<cc.winboll.studio.libaes.views.ASupportToolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/toolbar"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:gravity="center_vertical|center_horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ByTheWay"
android:textAppearance="?android:attr/textAppearanceLarge"/>
</LinearLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/splash_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/splashBackground"
android:clickable="true"
android:focusable="true">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:gravity="center"
android:padding="32dp">
<TextView
android:id="@+id/splash_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/splash_title"
android:textColor="@color/splashTitleText"
android:textSize="36sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/splash_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/splash_subtitle"
android:textColor="@color/splashSubtitleText"
android:textSize="14sp"
android:textStyle="italic"/>
</LinearLayout>
</FrameLayout>
</FrameLayout>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<Switch
android:id="@+id/sw_gps_receive_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="GPS定位消息接收服务"/>
</LinearLayout>
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_time"
android:textSize="14sp"
android:textColor="#333"/>
<TextView
android:id="@+id/tv_lat_lng"
android:textSize="13sp"
android:layout_marginTop="4dp"/>
<TextView
android:id="@+id/tv_other"
android:textSize="12sp"
android:layout_marginTop="2dp"
android:textColor="#666"/>
</LinearLayout>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_gps_list"
android:icon="@android:drawable/ic_menu_mapmode"
android:title="@string/action_gps_list"
app:showAsAction="never" />
<item
android:id="@+id/action_settings"
android:icon="@android:drawable/ic_menu_preferences"
android:title="@string/action_settings"
app:showAsAction="never" />
<item
android:id="@+id/action_about"
android:icon="@android:drawable/ic_menu_info_details"
android:title="@string/action_about"
app:showAsAction="never" />
</menu>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#42A5F5</color>
<color name="colorPrimaryDark">#1565C0</color>
<color name="colorAccent">#FFD740</color>
<color name="mainWindowBackgroundColor">#121212</color>
<color name="mainWindowTextColor">#E0E0E0</color>
<color name="toolbarTextColor">#FFFFFF</color>
<color name="toolbarBackgroundColor">#1E3A5F</color>
<color name="debugTextColor">#00E676</color>
<color name="colorTextBackgound">#121212</color>
</resources>
@@ -0,0 +1,29 @@
<resources>
<style name="MyAppTheme" parent="Theme.AppCompat.NoActionBar">
<item name="aSupportToolbar">@style/MyAESASupportToolbar</item>
<item name="themeDebug">@style/MyDebugActivityTheme</item>
<item name="aboutViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="aboutViewTextColor">?attr/mainWindowDarkTextColor</item>
<item name="aboutViewTitleColor">?attr/mainWindowDarkTextColor</item>
<item name="aboutViewDividerColor">?attr/mainWindowTextColor</item>
<item name="dialogBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="dialogTextColor">?attr/mainWindowDarkTextColor</item>
<item name="toolbarBackgroundColor">@color/toolbarBackgroundColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
<item name="textViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="textViewTextColor">?attr/mainWindowDarkTextColor</item>
<item name="editTextBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="editTextTextColor">?attr/mainWindowDarkTextColor</item>
<item name="scrollViewBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="activityBackgroundColor">?attr/mainWindowDarkBackgroundColor</item>
<item name="activityTextColor">?attr/mainWindowDarkTextColor</item>
<item name="mainWindowBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowTextColor">@color/mainWindowTextColor</item>
<item name="mainWindowDarkBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowDarkTextColor">@color/mainWindowTextColor</item>
</style>
<style name="MyAESASupportToolbar" parent="AESASupportToolbar" />
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">顺道说</string>
</resources>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#1565C0</color>
<color name="colorPrimaryDark">#0D47A1</color>
<color name="colorAccent">#FF6F00</color>
<color name="mainWindowBackgroundColor">#FAFAFA</color>
<color name="mainWindowTextColor">#212121</color>
<color name="toolbarTextColor">#FFFFFF</color>
<color name="toolbarBackgroundColor">#1565C0</color>
<color name="debugTextColor">#616161</color>
<color name="colorTextBackgound">#FAFAFA</color>
<color name="splashBackground">#1565C0</color>
<color name="splashTitleText">#FFFFFF</color>
<color name="splashSubtitleText">#B3FFFFFF</color>
</resources>
+10
View File
@@ -0,0 +1,10 @@
<resources>
<string name="app_name">ByTheWay</string>
<string name="action_settings">设置</string>
<string name="action_gps_list">GPS列表</string>
<string name="action_about">应用介绍</string>
<string name="app_description">The [ByTheWay] project is designed to quickly schedule task reminders related to time and location. Its main feature is the ability to quickly create task-related data.</string>
<string name="splash_title">ByTheWay</string>
<string name="splash_subtitle">By the way, I mention this for you.</string>
</resources>
+37
View File
@@ -0,0 +1,37 @@
<resources>
<style name="MyAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="aSupportToolbar">@style/MyAESASupportToolbar</item>
<item name="themeDebug">@style/MyDebugActivityTheme</item>
<item name="aboutViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="aboutViewTextColor">?attr/mainWindowTextColor</item>
<item name="aboutViewTitleColor">?attr/mainWindowTextColor</item>
<item name="aboutViewDividerColor">?attr/mainWindowDarkTextColor</item>
<item name="dialogBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="dialogTextColor">?attr/mainWindowTextColor</item>
<item name="toolbarBackgroundColor">@color/toolbarBackgroundColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
<item name="textViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="textViewTextColor">?attr/mainWindowTextColor</item>
<item name="editTextBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="editTextTextColor">?attr/mainWindowTextColor</item>
<item name="scrollViewBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="activityBackgroundColor">?attr/mainWindowBackgroundColor</item>
<item name="activityTextColor">?attr/mainWindowTextColor</item>
<item name="mainWindowBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowTextColor">@color/mainWindowTextColor</item>
<item name="mainWindowDarkBackgroundColor">@color/mainWindowBackgroundColor</item>
<item name="mainWindowDarkTextColor">@color/mainWindowTextColor</item>
</style>
<style name="MyDebugActivityTheme" parent="DebugActivityTheme">
<item name="colorTittle">?attr/mainWindowTextColor</item>
<item name="colorTittleBackgound">?attr/toolbarBackgroundColor</item>
<item name="colorText">?attr/debugTextColor</item>
<item name="colorTextBackgound">?attr/mainWindowBackgroundColor</item>
<item name="toolbarTextColor">@color/toolbarTextColor</item>
</style>
<style name="MyAESASupportToolbar" parent="AESASupportToolbar" />
</resources>
-8
View File
@@ -1,8 +0,0 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Jul 19 15:10:49 HKT 2026
stageCount=18
libraryProject=
baseVersion=15.0
publishVersion=15.0.17
buildCount=0
baseBetaVersion=15.0.18
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Put flavor specific strings here -->
<string name="app_name">Gallery+</string>
</resources>
-66
View File
@@ -1,66 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cc.winboll.studio.gallery">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/GalleryTheme"
android:resizeableActivity="true"
android:name=".App"
android:requestLegacyExternalStorage="true">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".SettingsActivity"
android:label="@string/settings_title"/>
<activity
android:name=".AlbumActivity"
android:label="@string/app_name"/>
<activity
android:name=".ImageViewerActivity"
android:label="@string/app_name"/>
<activity
android:name=".TrashActivity"
android:label="@string/trash"/>
<activity
android:name=".CropActivity"
android:label="调整封面"/>
<activity
android:name=".AboutActivity"
android:label="@string/about"/>
<meta-data
android:name="android.max_aspect"
android:value="4.0"/>
<activity android:name=".GlobalApplication$CrashActivity"/>
</application>
</manifest>
@@ -1,51 +0,0 @@
package cc.winboll.studio.gallery;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.APPInfo;
import cc.winboll.studio.libappbase.views.AboutView;
public class AboutActivity extends AppCompatActivity {
public static final String TAG = "AboutActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
AboutView aboutView = findViewById(R.id.aboutview);
aboutView.setAPPInfo(genDefaultAppInfo());
}
private APPInfo genDefaultAppInfo() {
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
String branchName = "gallery";
APPInfo appInfo = new APPInfo();
appInfo.setAppName("Gallery");
appInfo.setAppIcon(R.drawable.ic_winboll);
appInfo.setAppDescription(getString(R.string.app_description));
appInfo.setAppGitName("Gallery");
appInfo.setAppGitOwner("ZhanGSKen");
appInfo.setAppGitAPPBranch(branchName);
appInfo.setAppGitAPPSubProjectFolder(branchName);
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Gallery");
appInfo.setAppAPKName("Gallery");
appInfo.setAppAPKFolderName("Gallery");
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
return appInfo;
}
}
@@ -1,25 +0,0 @@
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; }
}
@@ -1,251 +0,0 @@
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.view.View;
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 com.google.android.material.floatingactionbutton.FloatingActionButton;
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;
private FloatingActionButton fabScrollTop;
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);
albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH);
albumName = getIntent().getStringExtra(EXTRA_ALBUM_NAME);
getSupportActionBar().setTitle(albumName);
prefs = new Preferences(this);
recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
adapter = new ImageAdapter();
adapter.setContext(this);
adapter.setAlbumPath(albumPath);
recyclerView.setAdapter(adapter);
fabScrollTop = findViewById(R.id.fab_scroll_top);
fabScrollTop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
recyclerView.scrollToPosition(0);
}
});
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
if (layoutManager != null) {
int firstVisible = layoutManager.findFirstVisibleItemPosition();
if (firstVisible > 0) {
fabScrollTop.setVisibility(View.VISIBLE);
} else {
fabScrollTop.setVisibility(View.GONE);
}
}
}
});
adapter.setOnImageClickListener(new OnImageClickListener() {
@Override
public void onImageClick(int position, ArrayList<Uri> urls, ArrayList<String> 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 String getSortOrder(int sortMode) {
switch (sortMode) {
case Preferences.SORT_TIME_ASC:
return android.provider.MediaStore.Images.Media.DATE_ADDED + " ASC";
case Preferences.SORT_NAME_DESC:
return android.provider.MediaStore.Images.Media.DISPLAY_NAME + " DESC";
case Preferences.SORT_NAME_ASC:
return android.provider.MediaStore.Images.Media.DISPLAY_NAME + " ASC";
case Preferences.SORT_TIME_DESC:
default:
return android.provider.MediaStore.Images.Media.DATE_ADDED + " DESC";
}
}
private void loadImages() {
LogUtils.d(TAG, "loadImages");
ArrayList<Uri> imageUrls = new ArrayList<>();
ArrayList<String> 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 + "%"};
int sortMode = prefs.getAlbumSortMode();
String sortOrder = getSortOrder(sortMode);
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_album, menu);
int sortMode = prefs.getAlbumSortMode();
int menuId = getSortMenuId(sortMode);
MenuItem item = menu.findItem(menuId);
if (item != null) {
item.setChecked(true);
}
MenuItem sortItem = menu.findItem(R.id.action_sort);
if (sortItem != null && sortItem.getSubMenu() != null) {
sortItem.getSubMenu().setGroupCheckable(0, true, true);
}
return true;
}
private int getSortMenuId(int sortMode) {
switch (sortMode) {
case Preferences.SORT_TIME_ASC:
return R.id.sort_time_asc;
case Preferences.SORT_NAME_DESC:
return R.id.sort_name_desc;
case Preferences.SORT_NAME_ASC:
return R.id.sort_name_asc;
case Preferences.SORT_TIME_DESC:
default:
return R.id.sort_time_desc;
}
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.sort_time_desc) {
prefs.setAlbumSortMode(Preferences.SORT_TIME_DESC);
item.setChecked(true);
loadImages();
return true;
} else if (itemId == R.id.sort_time_asc) {
prefs.setAlbumSortMode(Preferences.SORT_TIME_ASC);
item.setChecked(true);
loadImages();
return true;
} else if (itemId == R.id.sort_name_desc) {
prefs.setAlbumSortMode(Preferences.SORT_NAME_DESC);
item.setChecked(true);
loadImages();
return true;
} else if (itemId == R.id.sort_name_asc) {
prefs.setAlbumSortMode(Preferences.SORT_NAME_ASC);
item.setChecked(true);
loadImages();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onResume() {
super.onResume();
if (checkPermission()) {
loadImages();
}
if (adapter != null) {
adapter.refreshBg();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100 && resultCode == RESULT_OK) {
loadImages();
}
}
}
@@ -1,232 +0,0 @@
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.View.OnLongClickListener;
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.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import cc.winboll.studio.libappbase.LogUtils;
public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder> {
public static final String TAG = "AlbumAdapter";
private ArrayList<Album> albums = new ArrayList<>();
private OnAlbumClickListener listener;
private Preferences prefs;
private int bgType = 0;
private PinnedAlbumDbHelper pinnedDbHelper;
private OnCoverSizeListener coverSizeListener;
private boolean coverSizeReported = false;
private int getBgRes() {
switch (bgType) {
case 0:
return R.drawable.bg_checkerboard;
case 1:
return R.drawable.bg_white;
case 2:
return R.drawable.bg_black;
default:
return R.drawable.bg_checkerboard;
}
}
public interface OnAlbumClickListener {
void onAlbumClick(Album album);
}
public interface OnCoverSizeListener {
void onCoverSize(int width, int height);
}
public static final int DEFAULT_COVER_WIDTH = 240;
public static final int DEFAULT_COVER_HEIGHT = 120;
public void setOnAlbumClickListener(OnAlbumClickListener listener) {
this.listener = listener;
}
public void setOnCoverSizeListener(OnCoverSizeListener listener) {
this.coverSizeListener = listener;
}
public void setData(ArrayList<Album> albums) {
this.albums = albums;
LogUtils.d(TAG, "setData: " + albums.size() + " albums");
notifyDataSetChanged();
}
public void setContext(android.content.Context context) {
prefs = new Preferences(context);
bgType = prefs.getBgType();
pinnedDbHelper = PinnedAlbumDbHelper.getInstance(context);
}
public void refreshBg() {
if (prefs != null) {
bgType = prefs.getBgType();
}
notifyDataSetChanged();
}
public void refreshCover() {
notifyDataSetChanged();
}
public void refreshPinned() {
if (pinnedDbHelper != null && albums != null && !albums.isEmpty()) {
Set<String> pinnedPaths = new HashSet<String>();
pinnedPaths.addAll(Arrays.asList(pinnedDbHelper.getPinnedPaths()));
ArrayList<Album> pinned = new ArrayList<Album>();
ArrayList<Album> unpinned = new ArrayList<Album>();
for (Album album : albums) {
if (pinnedPaths.contains(album.getPath())) {
pinned.add(album);
} else {
unpinned.add(album);
}
}
albums.clear();
albums.addAll(pinned);
albums.addAll(unpinned);
notifyDataSetChanged();
}
}
private void showContextMenu(View view, final Album album) {
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
builder.setTitle(album.getName());
final boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath());
String[] items = isPinned ? new String[]{"取消置顶"} : new String[]{"置顶"};
builder.setItems(items, new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface dialog, int which) {
if (pinnedDbHelper != null) {
if (which == 0) {
if (isPinned) {
pinnedDbHelper.unpinAlbum(album.getPath());
} else {
pinnedDbHelper.pinAlbum(album.getPath());
}
refreshPinned();
}
}
}
});
builder.show();
}
@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 final ViewHolder holder, final int position) {
final Album album = albums.get(position);
LogUtils.d(TAG, "bind: " + album.getName() + ", cover=" + album.getCoverUri());
holder.coverImage.setBackgroundResource(getBgRes());
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(album.getPath());
holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE);
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
showContextMenu(holder.itemView, album);
return true;
}
});
holder.albumName.setText(album.getName());
holder.imageCount.setText(album.getImageCount() + " photos");
holder.itemView.post(new Runnable() {
@Override
public void run() {
if (!coverSizeReported && prefs != null) {
int savedWidth = prefs.getCoverWidth();
if (savedWidth == AlbumAdapter.DEFAULT_COVER_WIDTH) {
int width = holder.coverImage.getWidth();
int height = holder.coverImage.getHeight();
if (width > 0 && height > 0) {
prefs.setCoverWidth(width);
prefs.setCoverHeight(height);
float ratio = (float) width / height;
prefs.setCoverRatio(ratio);
coverSizeReported = true;
if (coverSizeListener != null) {
coverSizeListener.onCoverSize(width, height);
}
}
}
}
}
});
holder.itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onAlbumClick(album);
}
}
});
Uri coverUri = album.getCoverUri();
if (coverUri != null) {
String uriString = coverUri.toString();
LogUtils.d(TAG, "uri scheme: " + coverUri.getScheme() + ", path: " + uriString);
if ("file".equals(coverUri.getScheme())) {
File coverFile = new File(coverUri.getPath());
if (coverFile.exists()) {
Glide.with(holder.coverImage.getContext())
.load(coverFile)
.fitCenter()
.into(holder.coverImage);
return;
}
}
Glide.with(holder.coverImage.getContext())
.load(coverUri)
.fitCenter()
.into(holder.coverImage);
}
}
@Override
public int getItemCount() {
return albums.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView coverImage;
ImageView pinIcon;
TextView albumName;
TextView imageCount;
ViewHolder(View itemView) {
super(itemView);
coverImage = itemView.findViewById(R.id.album_cover);
pinIcon = itemView.findViewById(R.id.pin_icon);
albumName = itemView.findViewById(R.id.album_name);
imageCount = itemView.findViewById(R.id.image_count);
}
}
}
@@ -1,131 +0,0 @@
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 cc.winboll.studio.libappbase.LogUtils;
public class AlbumCoverDbHelper extends SQLiteOpenHelper {
public static final String TAG = "AlbumCoverDbHelper";
private static final String DB_NAME = "album_cover.db";
private static final int DB_VERSION = 2;
private static final String TABLE_NAME = "album_covers";
private static final String COLUMN_ALBUM_PATH = "album_path";
private static final String COLUMN_IMAGE_PATH = "image_path";
private static final String COLUMN_CROP_PATH = "crop_path";
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_ALBUM_PATH + " TEXT PRIMARY KEY, "
+ COLUMN_IMAGE_PATH + " TEXT, "
+ COLUMN_CROP_PATH + " TEXT)";
private static AlbumCoverDbHelper dbHelper;
public static AlbumCoverDbHelper getInstance(Context context) {
if (dbHelper == null) {
dbHelper = new AlbumCoverDbHelper(context.getApplicationContext());
}
return dbHelper;
}
public AlbumCoverDbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < 2) {
try {
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + COLUMN_CROP_PATH + " TEXT");
} catch (Exception e) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
}
}
public void setCoverWithCrop(String albumPath, String imagePath, String cropPath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_ALBUM_PATH, albumPath);
values.put(COLUMN_IMAGE_PATH, imagePath);
values.put(COLUMN_CROP_PATH, cropPath);
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
LogUtils.d(TAG, "setCoverWithCrop: album=" + albumPath + ", image=" + imagePath + ", crop=" + cropPath);
}
public void setCover(String albumPath, String imagePath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_ALBUM_PATH, albumPath);
values.put(COLUMN_IMAGE_PATH, imagePath);
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
LogUtils.d(TAG, "setCover: album=" + albumPath + ", image=" + imagePath);
}
public String getCover(String albumPath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_CROP_PATH, COLUMN_IMAGE_PATH},
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
String coverPath = null;
if (cursor != null) {
if (cursor.moveToFirst()) {
coverPath = cursor.getString(0);
if (coverPath == null) {
coverPath = cursor.getString(1);
}
}
cursor.close();
}
return coverPath;
}
public String getCropPath(String albumPath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_CROP_PATH},
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
String cropPath = null;
if (cursor != null) {
if (cursor.moveToFirst()) {
cropPath = cursor.getString(0);
}
cursor.close();
}
return cropPath;
}
public String getOriginalImagePath(String albumPath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_IMAGE_PATH},
COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath}, null, null, null);
String imagePath = null;
if (cursor != null) {
if (cursor.moveToFirst()) {
imagePath = cursor.getString(0);
}
cursor.close();
}
return imagePath;
}
public void clearCover(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.putNull(COLUMN_CROP_PATH);
db.update(TABLE_NAME, values, COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath});
LogUtils.d(TAG, "clearCover: " + albumPath);
}
public void deleteCover(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
db.delete(TABLE_NAME, COLUMN_ALBUM_PATH + " = ?", new String[]{albumPath});
LogUtils.d(TAG, "deleteCover: " + albumPath);
}
}
@@ -1,52 +0,0 @@
package cc.winboll.studio.gallery;
import cc.winboll.studio.gallery.utils.BackgroundUtils;
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
import cc.winboll.studio.libappbase.CrashActivity;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.ToastUtils;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* @Author ZhanGSKen<zhangsken@qq.com>
* @Date 2026/07/19
* @Describe 应用全局入口类(继承 GlobalWinBoLLApplication
* 负责应用初始化、全局资源管理与生命周期回调处理,是整个应用的核心入口
*/
public class App extends GlobalApplication {
public static final String TAG = "App";
@Override
public void onCreate() {
try {
super.onCreate();
WinBoLLActivityManager.init(this);
BackgroundUtils.initFromPreferences(this);
ToastUtils.init(getApplicationContext());
} catch (Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
pw.close();
String stackTraceStr = sw.toString();
CrashHandleNotifyUtils.handleUncaughtException(
this,
getPackageName(),
stackTraceStr,
CrashActivity.class
);
}
}
@Override
public void onTerminate() {
super.onTerminate();
ToastUtils.release();
}
}
@@ -1,313 +0,0 @@
package cc.winboll.studio.gallery;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.RectF;
import android.net.Uri;
import android.os.Bundle;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.RectF;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.View;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Random;
public class CropActivity extends AppCompatActivity {
public static final String TAG = "CropActivity";
public static final String EXTRA_IMAGE_URI = "image_uri";
public static final String EXTRA_IMAGE_PATH = "image_path";
public static final String EXTRA_ALBUM_PATH = "album_path";
public static final String EXTRA_CROP_WIDTH = "crop_width";
public static final String EXTRA_CROP_HEIGHT = "crop_height";
private CropCanvasView cropCanvasView;
private ZoomContainerView zoomContainer;
private Bitmap originalBitmap;
private String imagePath;
private String albumPath;
private int cropWidth = 240;
private int cropHeight = 120;
private float cropRatio = 2.0f;
private Preferences prefs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crop);
imagePath = getIntent().getStringExtra(EXTRA_IMAGE_PATH);
albumPath = getIntent().getStringExtra(EXTRA_ALBUM_PATH);
cropWidth = getIntent().getIntExtra(EXTRA_CROP_WIDTH, 240);
cropHeight = getIntent().getIntExtra(EXTRA_CROP_HEIGHT, 120);
prefs = new Preferences(this);
int bgType = prefs.getBgType();
if (cropWidth > 0 && cropHeight > 0) {
cropRatio = (float) cropWidth / cropHeight;
} else {
cropRatio = prefs.getCoverRatio();
cropWidth = (int) (120 * cropRatio);
cropHeight = 120;
}
findViewById(R.id.btn_close).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
findViewById(R.id.btn_done).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
saveCroppedCover();
}
});
findViewById(R.id.btn_info).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showCropInfoDialog();
}
});
findViewById(R.id.btn_change_bg).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(CropActivity.this, "修改剪裁背景颜色", Toast.LENGTH_SHORT).show();
}
});
zoomContainer = findViewById(R.id.zoom_container);
SeekBar seekBarZoom = findViewById(R.id.seekbar_zoom);
seekBarZoom.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (zoomContainer != null && fromUser) {
float scale = 0.1f + (progress / 100f) * 4.9f;
zoomContainer.setScaleFactor(scale);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});
cropCanvasView = findViewById(R.id.crop_canvas_view);
cropCanvasView.setBackgroundType(bgType);
cropCanvasView.setOnBackgroundColorChangedListener(new CropCanvasView.OnBackgroundColorChangedListener() {
@Override
public void onBackgroundColorChanged(int color) {
}
});
loadImage();
}
private void loadImage() {
try {
if (imagePath != null && new File(imagePath).exists()) {
originalBitmap = BitmapFactory.decodeFile(imagePath);
} else {
Uri imageUri = getIntent().getParcelableExtra(EXTRA_IMAGE_URI);
if (imageUri != null) {
originalBitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri);
}
}
if (originalBitmap != null) {
cropCanvasView.setImageBitmap(originalBitmap);
cropCanvasView.post(new Runnable() {
@Override
public void run() {
initCrop();
}
});
} else {
Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();
finish();
}
} catch (Exception e) {
LogUtils.e(TAG, "loadImage error: " + e.getMessage());
Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();
finish();
}
}
private void initCrop() {
cropCanvasView.initCanvas(originalBitmap.getWidth(), originalBitmap.getHeight(), cropRatio);
int viewW = cropCanvasView.getWidth();
int viewH = cropCanvasView.getHeight();
if (viewW > 0 && viewH > 0) {
cropCanvasView.scaleToView(viewW, viewH);
}
}
private void saveCroppedCover() {
if (originalBitmap == null || originalBitmap.isRecycled()) {
Toast.makeText(this, "Failed to get image", Toast.LENGTH_SHORT).show();
return;
}
try {
Bitmap canvasBitmap = cropCanvasView.getCanvasBitmap();
if (canvasBitmap == null || canvasBitmap.isRecycled()) {
Toast.makeText(this, "Failed to get canvas bitmap", Toast.LENGTH_SHORT).show();
return;
}
RectF cropRect = cropCanvasView.getCropRect();
int bmpX = (int) cropRect.left;
int bmpY = (int) cropRect.top;
int bmpW = (int) cropRect.width();
int bmpH = (int) cropRect.height();
bmpX = Math.max(0, Math.min(bmpX, canvasBitmap.getWidth() - 1));
bmpY = Math.max(0, Math.min(bmpY, canvasBitmap.getHeight() - 1));
bmpW = Math.max(1, Math.min(bmpW, canvasBitmap.getWidth() - bmpX));
bmpH = Math.max(1, Math.min(bmpH, canvasBitmap.getHeight() - bmpY));
LogUtils.d(TAG, "saveCroppedCover: cropRect=" + cropRect);
LogUtils.d(TAG, "saveCroppedCover: crop=(" + bmpX + "," + bmpY + "," + bmpW + "," + bmpH + ")");
LogUtils.d(TAG, "saveCroppedCover: canvas size=" + canvasBitmap.getWidth() + "x" + canvasBitmap.getHeight());
Bitmap cropped = Bitmap.createBitmap(canvasBitmap, bmpX, bmpY, bmpW, bmpH);
canvasBitmap.recycle();
LogUtils.d(TAG, "saveCroppedCover: cropped size=" + cropped.getWidth() + "x" + cropped.getHeight());
File coverDir = new File(getFilesDir(), "covers");
if (!coverDir.exists()) {
coverDir.mkdirs();
}
File coverFile = new File(coverDir, "cover_" + System.currentTimeMillis() + ".png");
LogUtils.d(TAG, "saveCroppedCover: cover file=" + coverFile.getAbsolutePath());
FileOutputStream fos = new FileOutputStream(coverFile);
cropped.compress(Bitmap.CompressFormat.PNG, 100, fos);
fos.close();
LogUtils.d(TAG, "saveCroppedCover: file exists=" + coverFile.exists() + ", length=" + coverFile.length());
cropped.recycle();
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
coverDbHelper.setCoverWithCrop(albumPath, imagePath, coverFile.getAbsolutePath());
LogUtils.d(TAG, "saveCroppedCover: cover saved to db, albumPath=" + albumPath);
Intent broadcastIntent = new Intent(Preferences.ACTION_COVER_UPDATED);
sendBroadcast(broadcastIntent);
Toast.makeText(this, "封面已保存", Toast.LENGTH_SHORT).show();
setResult(RESULT_OK);
finish();
} catch (Exception e) {
LogUtils.e(TAG, "saveCroppedCover error: " + e.getMessage());
Toast.makeText(this, "Failed to save cover", Toast.LENGTH_SHORT).show();
}
}
private void showCropInfoDialog() {
StringBuilder info = new StringBuilder();
info.append("=== 画布信息 ===\n");
info.append("画布宽度: ").append(cropCanvasView.getCanvasWidth()).append("px\n");
info.append("画布高度: ").append(cropCanvasView.getCanvasHeight()).append("px\n");
info.append("\n=== 背景类型 ===\n");
String[] bgNames = {"灰白相间", "全白", "全黑"};
info.append("背景: ").append(bgNames[cropCanvasView.getBackgroundType()]).append("\n");
int bgColor = cropCanvasView.getBackgroundColor();
info.append("背景颜色: #").append(String.format("%06X", bgColor & 0xFFFFFF)).append("\n");
info.append("拾取坐标: ").append(cropCanvasView.getLastPickImageX()).append(",")
.append(cropCanvasView.getLastPickImageY()).append("\n");
info.append("\n=== 裁剪结果 ===\n");
RectF cropRect = cropCanvasView.getCropRect();
if (cropRect != null) {
info.append("裁剪区域: ").append((int)cropRect.left).append(",")
.append((int)cropRect.top).append(",")
.append((int)cropRect.right).append(",")
.append((int)cropRect.bottom).append("\n");
info.append("裁剪宽度: ").append((int)cropRect.width()).append("px\n");
info.append("裁剪高度: ").append((int)cropRect.height()).append("px\n");
}
Bitmap canvasBitmap = cropCanvasView.getCanvasBitmap();
final Bitmap[] previewHolder = new Bitmap[1];
if (canvasBitmap != null && !canvasBitmap.isRecycled() && cropRect != null) {
int bmpX = Math.max(0, (int) cropRect.left);
int bmpY = Math.max(0, (int) cropRect.top);
int bmpW = Math.min((int) cropRect.width(), canvasBitmap.getWidth() - bmpX);
int bmpH = Math.min((int) cropRect.height(), canvasBitmap.getHeight() - bmpY);
if (bmpW > 0 && bmpH > 0) {
previewHolder[0] = Bitmap.createBitmap(canvasBitmap, bmpX, bmpY, bmpW, bmpH);
}
}
final Bitmap previewBitmap = previewHolder[0];
View dialogView = getLayoutInflater().inflate(R.layout.dialog_crop_info, null);
TextView infoText = dialogView.findViewById(R.id.info_text);
ImageView previewImage = dialogView.findViewById(R.id.preview_image);
final LinearLayout previewImageContainer = dialogView.findViewById(R.id.preview_image_container);
infoText.setText(info.toString());
if (previewBitmap != null) {
previewImage.setImageBitmap(previewBitmap);
}
previewImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int randomColor = 0xFF000000 | new Random().nextInt(0x00FFFFFF);
previewImageContainer.setBackgroundColor(randomColor);
}
});
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("裁剪信息")
.setView(dialogView)
.setPositiveButton("确定", null)
.create();
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
if (previewBitmap != null && !previewBitmap.isRecycled()) {
previewBitmap.recycle();
}
}
});
dialog.show();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (originalBitmap != null) {
originalBitmap.recycle();
originalBitmap = null;
}
}
}
@@ -1,750 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
public class CropCanvasView extends View {
public interface OnBackgroundColorChangedListener {
void onBackgroundColorChanged(int color);
}
public interface OnColorPickedListener {
void onColorPicked(int color);
}
public interface OnColorPickEndListener {
void onColorPickEnd();
}
private OnBackgroundColorChangedListener backgroundColorChangedListener;
private OnColorPickedListener colorPickedListener;
private OnColorPickEndListener colorPickEndListener;
public void setOnBackgroundColorChangedListener(OnBackgroundColorChangedListener listener) {
this.backgroundColorChangedListener = listener;
}
public void setOnColorPickedListener(OnColorPickedListener listener) {
this.colorPickedListener = listener;
}
public void setOnColorPickEndListener(OnColorPickEndListener listener) {
this.colorPickEndListener = listener;
}
private Paint imagePaint;
private Paint borderPaint;
private Paint cornerPaint;
private RectF cropRect;
private int touchArea = 50;
private float lastX, lastY;
private int activeCorner = -1;
private static final int CORNER_TOP_LEFT = 0;
private static final int CORNER_TOP_RIGHT = 1;
private static final int CORNER_BOTTOM_LEFT = 2;
private static final int CORNER_BOTTOM_RIGHT = 3;
private static final int CORNER_CENTER = 4;
private int imageWidth;
private int imageHeight;
private float coverRatio = 2.0f;
private int extendHeight;
private int extendWidth;
private int canvasWidth;
private int canvasHeight;
private float minSize = 50;
private RectF imageBounds = new RectF();
private RectF canvasBounds = new RectF();
private Bitmap originalBitmap;
private Bitmap canvasBitmap;
private float bitmapScale = 1.0f;
private Bitmap displayBitmap;
private RectF initialSpanRect;
private float initialSpan;
private int bgType = 2;
private Bitmap tileBitmap;
private BitmapShader tileShader;
private Paint bgPaint;
private int backgroundColor = Color.BLUE;
private boolean colorPickMode = false;
private int previewColor = 0;
private float lastPickX = 0;
private float lastPickY = 0;
private float pickX, pickY;
private float displayScale = 1.0f;
private float displayOffsetX = 0f;
private float displayOffsetY = 0f;
private static final int MAX_DISPLAY_SIZE = 2048;
private float containerScale = 1.0f;
public CropCanvasView(Context context) {
super(context);
init();
}
public CropCanvasView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CropCanvasView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
imagePaint = new Paint();
imagePaint.setFilterBitmap(true);
borderPaint = new Paint();
borderPaint.setColor(Color.parseColor("#CCAA00"));
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(3);
cornerPaint = new Paint();
cornerPaint.setColor(Color.WHITE);
cornerPaint.setStyle(Paint.Style.FILL);
bgType = 2;
bgPaint = new Paint();
initTileBitmap();
}
private void initTileBitmap() {
if (tileBitmap != null && !tileBitmap.isRecycled()) {
tileBitmap.recycle();
tileBitmap = null;
}
tileShader = null;
if (bgType == 0) {
Drawable drawable = getContext().getDrawable(R.drawable.bg_checkerboard);
if (drawable != null) {
int w = drawable.getIntrinsicWidth();
int h = drawable.getIntrinsicHeight();
if (w <= 0) w = 10;
if (h <= 0) h = 10;
tileBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(tileBitmap);
drawable.setBounds(0, 0, w, h);
drawable.draw(c);
tileShader = new BitmapShader(tileBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
}
}
}
public void setBackgroundType(int type) {
if (bgType != type) {
bgType = type;
initTileBitmap();
invalidate();
}
}
public int getBackgroundType() {
return bgType;
}
private void drawBackground(Canvas canvas) {
if (bgType == 0 && tileShader != null) {
bgPaint.setShader(tileShader);
canvas.drawRect(canvasBounds, bgPaint);
} else if (bgType == 1) {
bgPaint.setShader(null);
bgPaint.setColor(Color.WHITE);
canvas.drawRect(canvasBounds, bgPaint);
} else {
bgPaint.setShader(null);
bgPaint.setColor(Color.BLACK);
canvas.drawRect(canvasBounds, bgPaint);
}
}
public void setImageBitmap(Bitmap bitmap) {
if (displayBitmap != null && displayBitmap != originalBitmap) {
displayBitmap.recycle();
}
this.originalBitmap = bitmap;
createDisplayBitmap();
}
private void createDisplayBitmap() {
if (displayBitmap != null && displayBitmap != originalBitmap) {
displayBitmap.recycle();
displayBitmap = null;
}
if (originalBitmap == null || originalBitmap.isRecycled()) {
return;
}
int w = originalBitmap.getWidth();
int h = originalBitmap.getHeight();
float scale = 1.0f;
if (w > MAX_DISPLAY_SIZE || h > MAX_DISPLAY_SIZE) {
scale = Math.min((float) MAX_DISPLAY_SIZE / w, (float) MAX_DISPLAY_SIZE / h);
int newW = (int) (w * scale);
int newH = (int) (h * scale);
displayBitmap = Bitmap.createScaledBitmap(originalBitmap, newW, newH, true);
} else {
displayBitmap = originalBitmap;
}
displayBitmapScale = scale;
}
private float displayBitmapScale = 1.0f;
public Bitmap getOriginalBitmap() {
return originalBitmap;
}
public float getDisplayBitmapScale() {
return displayBitmapScale;
}
public void onContainerScaled() {
containerScale = 1.0f;
invalidate();
}
public void setContainerScale(float scale) {
if (scale > 0 && scale != containerScale) {
containerScale = scale;
invalidate();
}
}
public float getContainerScale() {
return containerScale;
}
public Bitmap getCanvasBitmap() {
if (canvasWidth <= 0 || canvasHeight <= 0) {
return null;
}
Bitmap canvasBmp = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(canvasBmp);
drawBackground(canvas);
if (displayBitmap != null && !displayBitmap.isRecycled()) {
if (displayBitmap == originalBitmap) {
canvas.drawBitmap(displayBitmap, imageBounds.left, imageBounds.top, imagePaint);
} else {
float invScale = 1f / displayBitmapScale;
canvas.save();
canvas.scale(invScale, invScale);
canvas.drawBitmap(displayBitmap, imageBounds.left * displayBitmapScale,
imageBounds.top * displayBitmapScale, imagePaint);
canvas.restore();
}
}
return canvasBmp;
}
public void initCanvas(int imgWidth, int imgHeight, float ratio) {
this.imageWidth = imgWidth;
this.imageHeight = imgHeight;
this.coverRatio = ratio;
float imgRatio = (float) imgWidth / imgHeight;
if (coverRatio >= imgRatio) {
extendHeight = imgHeight;
extendWidth = (int) (imgHeight * coverRatio);
} else {
extendWidth = imgWidth;
extendHeight = (int) (imgWidth / coverRatio);
}
canvasHeight = Math.max(imageHeight, extendHeight);
canvasWidth = Math.max(imageWidth, extendWidth);
float left = (canvasWidth - imageWidth) / 2f;
float top = (canvasHeight - imageHeight) / 2f;
imageBounds.set(left, top, left + imageWidth, top + imageHeight);
canvasBounds.set(0, 0, canvasWidth, canvasHeight);
cropRect = new RectF(canvasBounds);
requestLayout();
invalidate();
}
public void getDisplayMatrix(Matrix matrix) {
if (canvasWidth <= 0 || canvasHeight <= 0 || getWidth() <= 0 || getHeight() <= 0) {
return;
}
matrix.reset();
float displayScaleX = (float) getWidth() / canvasWidth;
float displayScaleY = (float) getHeight() / canvasHeight;
displayScale = Math.min(displayScaleX, displayScaleY);
displayOffsetX = (getWidth() - canvasWidth * displayScale) / 2f;
displayOffsetY = (getHeight() - canvasHeight * displayScale) / 2f;
matrix.postTranslate(displayOffsetX, displayOffsetY);
matrix.postScale(displayScale, displayScale);
}
public float screenToImageX(float screenX) {
return (screenX - displayOffsetX) / displayScale;
}
public float screenToImageY(float screenY) {
return (screenY - displayOffsetY) / displayScale;
}
public float imageToScreenX(float imageX) {
return imageX * displayScale + displayOffsetX;
}
public float imageToScreenY(float imageY) {
return imageY * displayScale + displayOffsetY;
}
public int getImageColorAt(float screenX, float screenY) {
Bitmap bmp = (displayBitmap != null) ? displayBitmap : originalBitmap;
if (bmp == null || bmp.isRecycled()) {
return Color.TRANSPARENT;
}
float imgX = screenToImageX(screenX);
float imgY = screenToImageY(screenY);
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
if (bmpX >= 0 && bmpX < bmp.getWidth() &&
bmpY >= 0 && bmpY < bmp.getHeight()) {
return bmp.getPixel(bmpX, bmpY);
}
return Color.TRANSPARENT;
}
public void setBackgroundColor(int color) {
this.backgroundColor = color;
if (backgroundColorChangedListener != null) {
backgroundColorChangedListener.onBackgroundColorChanged(color);
}
invalidate();
}
public int getBackgroundColor() {
return backgroundColor;
}
public int getPreviewColor() {
return previewColor;
}
public float getLastPickX() {
return lastPickX;
}
public float getLastPickY() {
return lastPickY;
}
public int getLastPickImageX() {
if (originalBitmap == null || originalBitmap.isRecycled()) {
return 0;
}
float imgX = screenToImageX(lastPickX);
float imgY = screenToImageY(lastPickY);
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
return Math.max(0, Math.min(bmpX, originalBitmap.getWidth() - 1));
}
public int getLastPickImageY() {
if (originalBitmap == null || originalBitmap.isRecycled()) {
return 0;
}
float imgX = screenToImageX(lastPickX);
float imgY = screenToImageY(lastPickY);
int bmpX = (int) ((imgX - imageBounds.left) * displayBitmapScale);
int bmpY = (int) ((imgY - imageBounds.top) * displayBitmapScale);
return Math.max(0, Math.min(bmpY, originalBitmap.getHeight() - 1));
}
public void setColorPickMode(boolean enable) {
this.colorPickMode = enable;
if (enable) {
pickX = -1;
pickY = -1;
}
invalidate();
}
public boolean isColorPickMode() {
return colorPickMode;
}
public void scaleToView(int viewWidth, int viewHeight) {
if (viewWidth > 0 && viewHeight > 0 && canvasWidth > 0 && canvasHeight > 0) {
float scale = Math.max((float) viewWidth / canvasWidth, (float) viewHeight / canvasHeight);
int oldCanvasW = canvasWidth;
int oldCanvasH = canvasHeight;
canvasWidth = (int) (canvasWidth * scale);
canvasHeight = (int) (canvasHeight * scale);
float left = (canvasWidth - imageWidth) / 2f;
float top = (canvasHeight - imageHeight) / 2f;
imageBounds.set(left, top, left + imageWidth, top + imageHeight);
canvasBounds.set(0, 0, canvasWidth, canvasHeight);
if (cropRect != null) {
float scaleX = (float) canvasWidth / oldCanvasW;
float scaleY = (float) canvasHeight / oldCanvasH;
cropRect.left *= scaleX;
cropRect.top *= scaleY;
cropRect.right *= scaleX;
cropRect.bottom *= scaleY;
}
requestLayout();
invalidate();
}
}
public RectF getCropRect() {
return new RectF(cropRect);
}
public int getCanvasWidth() {
return canvasWidth;
}
public int getCanvasHeight() {
return canvasHeight;
}
public RectF getCanvasBounds() {
return new RectF(canvasBounds);
}
public RectF getImageBounds() {
return new RectF(imageBounds);
}
public float getBitmapScale() {
return displayScale;
}
public float getDisplayScale() {
return displayScale;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w = canvasWidth > 0 ? canvasWidth : 0;
int h = canvasHeight > 0 ? canvasHeight : 0;
setMeasuredDimension(w, h);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
getDisplayMatrix(new Matrix());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (cropRect == null) return;
if (displayBitmap != null && !displayBitmap.isRecycled()) {
canvas.save();
Matrix matrix = new Matrix();
getDisplayMatrix(matrix);
canvas.concat(matrix);
drawBackground(canvas);
if (displayBitmap == originalBitmap) {
canvas.drawBitmap(displayBitmap, imageBounds.left, imageBounds.top, imagePaint);
} else {
float invScale = 1f / displayBitmapScale;
canvas.save();
canvas.scale(invScale, invScale);
canvas.drawBitmap(displayBitmap, imageBounds.left * displayBitmapScale,
imageBounds.top * displayBitmapScale, imagePaint);
canvas.restore();
}
canvas.restore();
if (containerScale != 1.0f) {
canvas.save();
canvas.scale(containerScale, containerScale);
canvas.drawRect(cropRect, borderPaint);
float cornerRadius = 12f / containerScale;
canvas.drawCircle(cropRect.left, cropRect.top, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, cornerRadius, cornerPaint);
canvas.restore();
} else {
canvas.drawRect(cropRect, borderPaint);
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
}
} else {
if (containerScale != 1.0f) {
canvas.save();
canvas.scale(containerScale, containerScale);
canvas.drawRect(cropRect, borderPaint);
float cornerRadius = 12f / containerScale;
canvas.drawCircle(cropRect.left, cropRect.top, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, cornerRadius, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, cornerRadius, cornerPaint);
canvas.restore();
} else {
canvas.drawRect(cropRect, borderPaint);
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (cropRect == null || canvasBounds.isEmpty()) return super.onTouchEvent(event);
if (event.getPointerCount() == 2) {
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_POINTER_DOWN) {
initialSpan = getSpan(event);
return true;
} else if (action == MotionEvent.ACTION_MOVE) {
float span = getSpan(event);
if (initialSpan > 0) {
float scale = span / initialSpan;
float centerX = cropRect.centerX();
float centerY = cropRect.centerY();
float newWidth = cropRect.width() * scale;
float newHeight = newWidth / coverRatio;
newWidth = Math.max(minSize, Math.min(newWidth, canvasBounds.width()));
newHeight = newWidth / coverRatio;
float newLeft = centerX - newWidth / 2;
float newTop = centerY - newHeight / 2;
float newRight = centerX + newWidth / 2;
float newBottom = centerY + newHeight / 2;
if (newLeft >= canvasBounds.left && newRight <= canvasBounds.right &&
newTop >= canvasBounds.top && newBottom <= canvasBounds.bottom) {
cropRect.set(newLeft, newTop, newRight, newBottom);
}
initialSpan = span;
invalidate();
}
return true;
}
}
float x = event.getX();
float y = event.getY();
RectF bounds = canvasBounds;
if (containerScale != 1.0f) {
x = x / containerScale;
y = y / containerScale;
bounds = new RectF(
canvasBounds.left,
canvasBounds.top,
canvasBounds.right,
canvasBounds.bottom
);
}
if (colorPickMode) {
if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) {
if (originalBitmap != null && !originalBitmap.isRecycled()) {
float imgX = screenToImageX(x);
float imgY = screenToImageY(y);
if (imageBounds.contains(imgX, imgY)) {
previewColor = getImageColorAt(x, y);
} else if (canvasBounds.contains(x, y)) {
previewColor = (bgType == 1) ? Color.WHITE : Color.BLACK;
} else {
previewColor = Color.TRANSPARENT;
}
lastPickX = x;
lastPickY = y;
if (backgroundColorChangedListener != null) {
backgroundColorChangedListener.onBackgroundColorChanged(previewColor);
}
}
return true;
}
if (event.getAction() == MotionEvent.ACTION_UP) {
if (canvasBounds.contains(x, y)) {
int pickedColor;
float imgX = screenToImageX(x);
float imgY = screenToImageY(y);
if (imageBounds.contains(imgX, imgY)) {
int colorAtPoint = getImageColorAt(x, y);
pickedColor = colorAtPoint;
backgroundColor = colorAtPoint;
} else {
pickedColor = (bgType == 1) ? Color.WHITE : Color.BLACK;
}
if (colorPickedListener != null) {
colorPickedListener.onColorPicked(pickedColor);
}
}
if (colorPickEndListener != null) {
colorPickEndListener.onColorPickEnd();
}
previewColor = 0;
invalidate();
return true;
}
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
activeCorner = getActiveCorner(x, y);
return true;
case MotionEvent.ACTION_MOVE:
float dx = x - lastX;
float dy = y - lastY;
if (activeCorner == CORNER_CENTER) {
float newLeft = cropRect.left + dx;
float newTop = cropRect.top + dy;
float newRight = cropRect.right + dx;
float newBottom = cropRect.bottom + dy;
if (newLeft >= bounds.left && newRight <= bounds.right) {
cropRect.left = newLeft;
cropRect.right = newRight;
}
if (newTop >= bounds.top && newBottom <= bounds.bottom) {
cropRect.top = newTop;
cropRect.bottom = newBottom;
}
} else if (activeCorner == CORNER_TOP_LEFT) {
adjustCorner(cropRect.left + dx, cropRect.top + dy, true, true);
} else if (activeCorner == CORNER_TOP_RIGHT) {
adjustCorner(cropRect.right + dx, cropRect.top + dy, false, true);
} else if (activeCorner == CORNER_BOTTOM_LEFT) {
adjustCorner(cropRect.left + dx, cropRect.bottom + dy, true, false);
} else if (activeCorner == CORNER_BOTTOM_RIGHT) {
adjustCorner(cropRect.right + dx, cropRect.bottom + dy, false, false);
}
lastX = x;
lastY = y;
invalidate();
return true;
case MotionEvent.ACTION_UP:
activeCorner = -1;
return true;
}
return super.onTouchEvent(event);
}
private void adjustCorner(float nx, float ny, boolean left, boolean top) {
float newWidth;
float newLeft = cropRect.left;
float newTop = cropRect.top;
float newRight = cropRect.right;
float newBottom = cropRect.bottom;
if (left) {
newWidth = cropRect.width() - (nx - cropRect.left);
newLeft = Math.max(canvasBounds.left, Math.min(nx, cropRect.right - minSize));
} else {
newWidth = nx - cropRect.left;
newRight = Math.min(canvasBounds.right, Math.max(nx, cropRect.left + minSize));
newLeft = cropRect.left;
}
float newHeight = newWidth / coverRatio;
if (top) {
newTop = Math.max(canvasBounds.top, Math.min(cropRect.bottom - minSize, cropRect.bottom - newHeight));
newBottom = newTop + newHeight;
} else {
newBottom = Math.min(canvasBounds.bottom, Math.max(cropRect.top + minSize, cropRect.top + newHeight));
newTop = newBottom - newHeight;
}
if (left) {
cropRect.left = newLeft;
cropRect.right = newLeft + newWidth;
} else {
cropRect.right = newRight;
cropRect.left = newRight - newWidth;
}
if (top) {
cropRect.top = newTop;
cropRect.bottom = newTop + newHeight;
} else {
cropRect.bottom = newBottom;
cropRect.top = newBottom - newHeight;
}
}
private int getActiveCorner(float x, float y) {
float dx = Math.abs(x - cropRect.left);
float dy = Math.abs(y - cropRect.top);
float dx2 = Math.abs(x - cropRect.right);
float dy2 = Math.abs(y - cropRect.bottom);
if (dx <= touchArea && dy <= touchArea) {
return CORNER_TOP_LEFT;
}
if (dx2 <= touchArea && dy <= touchArea) {
return CORNER_TOP_RIGHT;
}
if (dx <= touchArea && dy2 <= touchArea) {
return CORNER_BOTTOM_LEFT;
}
if (dx2 <= touchArea && dy2 <= touchArea) {
return CORNER_BOTTOM_RIGHT;
}
if (x > cropRect.left && x < cropRect.right && y > cropRect.top && y < cropRect.bottom) {
return CORNER_CENTER;
}
return -1;
}
private float getSpan(MotionEvent event) {
float x0 = event.getX(0);
float y0 = event.getY(0);
float x1 = event.getX(1);
float y1 = event.getY(1);
return (float) Math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
}
public int getColorAt(float x, float y) {
return getImageColorAt(x, y);
}
}
@@ -1,224 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
public class CropOverlayView extends View {
public interface OnCanvasSizeChangedListener {
void onCanvasSizeChanged(RectF canvasBounds);
}
private OnCanvasSizeChangedListener canvasSizeChangedListener;
private Paint borderPaint;
private Paint cornerPaint;
private RectF cropRect;
private int touchArea = 50;
private float lastX, lastY;
private int activeCorner = -1;
private static final int CORNER_TOP_LEFT = 0;
private static final int CORNER_TOP_RIGHT = 1;
private static final int CORNER_BOTTOM_LEFT = 2;
private static final int CORNER_BOTTOM_RIGHT = 3;
private static final int CORNER_CENTER = 4;
private float targetRatio = 2.0f;
private RectF canvasBounds = new RectF();
private float minSize = 50;
public CropOverlayView(Context context) {
super(context);
initPaints();
}
public CropOverlayView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaints();
}
public CropOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaints();
}
private void initPaints() {
borderPaint = new Paint();
borderPaint.setColor(Color.parseColor("#CCAA00"));
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(3);
cornerPaint = new Paint();
cornerPaint.setColor(Color.WHITE);
cornerPaint.setStyle(Paint.Style.FILL);
}
public void setTargetRatio(float ratio) {
this.targetRatio = ratio;
}
public float getTargetRatio() {
return targetRatio;
}
public void setOnCanvasSizeChangedListener(OnCanvasSizeChangedListener listener) {
this.canvasSizeChangedListener = listener;
}
public void initCanvas(int canvasW, int canvasH) {
canvasBounds.set(0, 0, canvasW, canvasH);
cropRect = new RectF(0, 0, canvasW, canvasH);
if (canvasSizeChangedListener != null) {
canvasSizeChangedListener.onCanvasSizeChanged(new RectF(canvasBounds));
}
invalidate();
}
public RectF getCropRect() {
return new RectF(cropRect);
}
public RectF getCanvasBounds() {
return new RectF(canvasBounds);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.TRANSPARENT);
if (cropRect != null) {
canvas.drawRect(cropRect, borderPaint);
canvas.drawCircle(cropRect.left, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.top, 12, cornerPaint);
canvas.drawCircle(cropRect.left, cropRect.bottom, 12, cornerPaint);
canvas.drawCircle(cropRect.right, cropRect.bottom, 12, cornerPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (cropRect == null || canvasBounds.isEmpty()) return false;
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
activeCorner = getActiveCorner(x, y);
return true;
case MotionEvent.ACTION_MOVE:
float dx = x - lastX;
float dy = y - lastY;
if (activeCorner == CORNER_CENTER) {
float newLeft = cropRect.left + dx;
float newTop = cropRect.top + dy;
float newRight = cropRect.right + dx;
float newBottom = cropRect.bottom + dy;
if (newLeft >= canvasBounds.left && newRight <= canvasBounds.right) {
cropRect.left = newLeft;
cropRect.right = newRight;
}
if (newTop >= canvasBounds.top && newBottom <= canvasBounds.bottom) {
cropRect.top = newTop;
cropRect.bottom = newBottom;
}
} else if (activeCorner == CORNER_TOP_LEFT) {
adjustCorner(cropRect.left + dx, cropRect.top + dy, true, true);
} else if (activeCorner == CORNER_TOP_RIGHT) {
adjustCorner(cropRect.right + dx, cropRect.top + dy, false, true);
} else if (activeCorner == CORNER_BOTTOM_LEFT) {
adjustCorner(cropRect.left + dx, cropRect.bottom + dy, true, false);
} else if (activeCorner == CORNER_BOTTOM_RIGHT) {
adjustCorner(cropRect.right + dx, cropRect.bottom + dy, false, false);
}
lastX = x;
lastY = y;
invalidate();
return true;
case MotionEvent.ACTION_UP:
activeCorner = -1;
return true;
}
return super.onTouchEvent(event);
}
private void adjustCorner(float nx, float ny, boolean left, boolean top) {
float newWidth;
float newLeft = cropRect.left;
float newTop = cropRect.top;
float newRight = cropRect.right;
float newBottom = cropRect.bottom;
if (left) {
newWidth = cropRect.width() - (nx - cropRect.left);
newLeft = Math.max(canvasBounds.left, Math.min(nx, cropRect.right - minSize));
} else {
newWidth = nx - cropRect.left;
newRight = Math.min(canvasBounds.right, Math.max(nx, cropRect.left + minSize));
newLeft = cropRect.left;
}
float newHeight = newWidth / targetRatio;
if (top) {
newTop = Math.max(canvasBounds.top, Math.min(cropRect.bottom - minSize, cropRect.bottom - newHeight));
newBottom = newTop + newHeight;
} else {
newBottom = Math.min(canvasBounds.bottom, Math.max(cropRect.top + minSize, cropRect.top + newHeight));
newTop = newBottom - newHeight;
}
if (left) {
cropRect.left = newLeft;
cropRect.right = newLeft + newWidth;
} else {
cropRect.right = newRight;
cropRect.left = newRight - newWidth;
}
if (top) {
cropRect.top = newTop;
cropRect.bottom = newTop + newHeight;
} else {
cropRect.bottom = newBottom;
cropRect.top = newBottom - newHeight;
}
}
private int getActiveCorner(float x, float y) {
if (Math.abs(x - cropRect.left) <= touchArea && Math.abs(y - cropRect.top) <= touchArea) {
return CORNER_TOP_LEFT;
}
if (Math.abs(x - cropRect.right) <= touchArea && Math.abs(y - cropRect.top) <= touchArea) {
return CORNER_TOP_RIGHT;
}
if (Math.abs(x - cropRect.left) <= touchArea && Math.abs(y - cropRect.bottom) <= touchArea) {
return CORNER_BOTTOM_LEFT;
}
if (Math.abs(x - cropRect.right) <= touchArea && Math.abs(y - cropRect.bottom) <= touchArea) {
return CORNER_BOTTOM_RIGHT;
}
if (cropRect.contains(x, y)) {
return CORNER_CENTER;
}
return -1;
}
}
@@ -1,247 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.DialogInterface;
import android.content.Intent;
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 java.util.Collections;
import java.util.Comparator;
import cc.winboll.studio.libappbase.LogUtils;
public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
public static final String TAG = "ImageAdapter";
private ArrayList<Uri> imageUrls = new ArrayList<>();
private ArrayList<String> imagePaths = new ArrayList<>();
private OnImageClickListener listener;
private int bgType = 0;
private Preferences prefs;
private PinnedImageDbHelper pinnedDbHelper;
private AlbumCoverDbHelper coverDbHelper;
private String albumPath;
private int getBgRes() {
switch (bgType) {
case 0:
return R.drawable.bg_checkerboard;
case 1:
return R.drawable.bg_white;
case 2:
return R.drawable.bg_black;
default:
return R.drawable.bg_checkerboard;
}
}
public interface OnImageClickListener {
void onImageClick(int position, ArrayList<Uri> urls, ArrayList<String> paths);
}
public void setOnImageClickListener(OnImageClickListener listener) {
this.listener = listener;
}
public void setData(ArrayList<Uri> urls, ArrayList<String> paths) {
this.imageUrls = urls;
this.imagePaths = paths;
sortPinnedFirst();
LogUtils.d(TAG, "setData: " + urls.size() + " images");
notifyDataSetChanged();
}
private void sortPinnedFirst() {
if (pinnedDbHelper == null || imagePaths.isEmpty()) {
return;
}
ArrayList<Uri> pinnedUrls = new ArrayList<>();
ArrayList<String> pinnedPaths = new ArrayList<>();
ArrayList<Uri> unpinnedUrls = new ArrayList<>();
ArrayList<String> unpinnedPaths = new ArrayList<>();
for (int i = 0; i < imagePaths.size(); i++) {
String path = imagePaths.get(i);
if (pinnedDbHelper.isPinned(path)) {
pinnedUrls.add(imageUrls.get(i));
pinnedPaths.add(path);
} else {
unpinnedUrls.add(imageUrls.get(i));
unpinnedPaths.add(path);
}
}
imageUrls.clear();
imagePaths.clear();
imageUrls.addAll(pinnedUrls);
imageUrls.addAll(unpinnedUrls);
imagePaths.addAll(pinnedPaths);
imagePaths.addAll(unpinnedPaths);
}
public void setContext(android.content.Context context) {
prefs = new Preferences(context);
bgType = prefs.getBgType();
pinnedDbHelper = PinnedImageDbHelper.getInstance(context);
coverDbHelper = AlbumCoverDbHelper.getInstance(context);
}
public void setAlbumPath(String albumPath) {
this.albumPath = albumPath;
}
public int getCropWidth() {
return prefs != null ? prefs.getCoverWidth() : 240;
}
public int getCropHeight() {
return prefs != null ? prefs.getCoverHeight() : 120;
}
public void refreshBg() {
if (prefs != null) {
bgType = prefs.getBgType();
notifyDataSetChanged();
}
}
public void refreshPinned() {
if (pinnedDbHelper == null || imagePaths.isEmpty()) {
return;
}
sortPinnedFirst();
notifyDataSetChanged();
}
private void showContextMenu(final View view, final int position) {
final String imagePath = imagePaths.get(position);
final Uri imageUri = imageUrls.get(position);
final boolean[] isPinned = {pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath)};
final boolean[] isCover = {false};
if (coverDbHelper != null && albumPath != null) {
String originalPath = coverDbHelper.getOriginalImagePath(albumPath);
isCover[0] = originalPath != null && originalPath.equals(imagePath);
}
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(view.getContext());
builder.setTitle("Image");
String[] items;
if (isPinned[0]) {
if (isCover[0]) {
items = new String[]{"取消置顶", "调整封面", "取消封面"};
} else {
items = new String[]{"取消置顶", "设置为封面"};
}
} else {
if (isCover[0]) {
items = new String[]{"置顶", "调整封面", "取消封面"};
} else {
items = new String[]{"置顶", "设置为封面"};
}
}
builder.setItems(items, new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface dialog, int which) {
if (which == 0) {
if (pinnedDbHelper != null) {
if (isPinned[0]) {
pinnedDbHelper.unpinImage(imagePath);
} else {
pinnedDbHelper.pinImage(imagePath);
}
refreshPinned();
}
} else if (which == 1) {
if (coverDbHelper != null && albumPath != null) {
Intent cropIntent = new Intent(view.getContext(), CropActivity.class);
cropIntent.putExtra(CropActivity.EXTRA_IMAGE_URI, imageUri);
cropIntent.putExtra(CropActivity.EXTRA_IMAGE_PATH, imagePath);
cropIntent.putExtra(CropActivity.EXTRA_ALBUM_PATH, albumPath);
cropIntent.putExtra(CropActivity.EXTRA_CROP_WIDTH, getCropWidth());
cropIntent.putExtra(CropActivity.EXTRA_CROP_HEIGHT, getCropHeight());
((AlbumActivity) view.getContext()).startActivityForResult(cropIntent, 100);
}
} else if (which == 2) {
if (coverDbHelper != null && albumPath != null && isCover[0]) {
coverDbHelper.deleteCover(albumPath);
notifyDataSetChanged();
}
}
}
});
builder.show();
}
@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 final ViewHolder holder, final int position) {
holder.imageView.setBackgroundResource(getBgRes());
Glide.with(holder.imageView.getContext())
.load(imageUrls.get(position))
.centerCrop()
.into(holder.imageView);
final ArrayList<Uri> urls = imageUrls;
final ArrayList<String> paths = imagePaths;
final String imagePath = imagePaths.get(position);
boolean isPinned = pinnedDbHelper != null && pinnedDbHelper.isPinned(imagePath);
holder.pinIcon.setVisibility(isPinned ? View.VISIBLE : View.GONE);
boolean isCover = false;
if (coverDbHelper != null && albumPath != null) {
String originalPath = coverDbHelper.getOriginalImagePath(albumPath);
isCover = originalPath != null && originalPath.equals(imagePath);
}
holder.coverIcon.setVisibility(isCover ? View.VISIBLE : View.GONE);
holder.itemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (listener != null) {
listener.onImageClick(position, urls, paths);
}
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
showContextMenu(holder.itemView, position);
return true;
}
});
}
@Override
public int getItemCount() {
return imageUrls.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;
ImageView pinIcon;
ImageView coverIcon;
ViewHolder(View itemView) {
super(itemView);
imageView = itemView.findViewById(R.id.image);
pinIcon = itemView.findViewById(R.id.pin_icon);
coverIcon = itemView.findViewById(R.id.cover_icon);
}
}
}
@@ -1,57 +0,0 @@
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<Uri> imageUrls;
public ImagePagerAdapter(ArrayList<Uri> 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);
view.setBackgroundResource(R.color.black);
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;
}
}
@@ -1,397 +0,0 @@
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 android.widget.Toast;
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<Uri> imageUrls;
private ArrayList<String> imagePaths;
private int currentPosition;
private ViewPager viewPager;
private View toolbar;
private ImageButton btnBack;
private ImageButton btnDelete;
private ImageButton btnShare;
private ImageButton btnInfo;
private ImageButton btnGallery;
private GestureDetector gestureDetector;
private TrashManager trashManager;
private Preferences prefs;
@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);
prefs = new Preferences(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);
btnInfo = findViewById(R.id.btn_info);
btnGallery = findViewById(R.id.btn_gallery);
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();
}
});
btnInfo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showImageInfo();
}
});
btnGallery.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(ImageViewerActivity.this, "Gallery", Toast.LENGTH_SHORT).show();
if (imageUrls != null && currentPosition >= 0 && currentPosition < imageUrls.size()) {
Uri imageUri = imageUrls.get(currentPosition);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(imageUri, "image/*");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(intent, "打开相册"));
}
}
});
}
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"));
}
}
private void showImageInfo() {
if (currentPosition < 0 || currentPosition >= imageUrls.size()) {
return;
}
String imagePath = "";
if (imagePaths != null && currentPosition < imagePaths.size()) {
imagePath = imagePaths.get(currentPosition);
} else {
imagePath = getPathFromUri(imageUrls.get(currentPosition));
}
File imageFile = new File(imagePath);
if (!imageFile.exists()) {
imageFile = new File(imagePath);
}
android.widget.LinearLayout layout = new android.widget.LinearLayout(this);
layout.setOrientation(android.widget.LinearLayout.VERTICAL);
layout.setPadding(48, 32, 48, 32);
android.widget.TextView labelPath = new android.widget.TextView(this);
labelPath.setText("Path:");
labelPath.setTextColor(getColor(android.R.color.darker_gray));
labelPath.setTextSize(14);
labelPath.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelPath);
android.widget.TextView valuePath = new android.widget.TextView(this);
valuePath.setText(imagePath);
valuePath.setTextColor(getColor(android.R.color.black));
valuePath.setTextSize(14);
valuePath.setTextIsSelectable(true);
layout.addView(valuePath);
if (imageFile.exists()) {
long sizeBytes = imageFile.length();
String size;
if (sizeBytes < 1024) {
size = sizeBytes + " B";
} else if (sizeBytes < 1024 * 1024) {
size = String.format("%.2f KB", sizeBytes / 1024.0);
} else {
size = String.format("%.2f MB", sizeBytes / (1024.0 * 1024.0));
}
android.widget.TextView labelSize = new android.widget.TextView(this);
labelSize.setText("Size:");
labelSize.setTextColor(getColor(android.R.color.darker_gray));
labelSize.setTextSize(14);
labelSize.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelSize);
android.widget.TextView valueSize = new android.widget.TextView(this);
valueSize.setText(size);
valueSize.setTextColor(getColor(android.R.color.black));
valueSize.setTextSize(14);
valueSize.setTextIsSelectable(true);
layout.addView(valueSize);
}
try {
android.graphics.BitmapFactory.Options options = new android.graphics.BitmapFactory.Options();
options.inJustDecodeBounds = true;
android.graphics.BitmapFactory.decodeFile(imagePath, options);
if (options.outWidth > 0 && options.outHeight > 0) {
android.widget.TextView labelPixels = new android.widget.TextView(this);
labelPixels.setText("Pixels:");
labelPixels.setTextColor(getColor(android.R.color.darker_gray));
labelPixels.setTextSize(14);
labelPixels.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelPixels);
android.widget.TextView valuePixels = new android.widget.TextView(this);
valuePixels.setText(options.outWidth + " x " + options.outHeight);
valuePixels.setTextColor(getColor(android.R.color.black));
valuePixels.setTextSize(14);
valuePixels.setTextIsSelectable(true);
layout.addView(valuePixels);
}
} catch (Exception e) {
LogUtils.e(TAG, "get pixels error: " + e.getMessage());
}
try {
String[] projection = {
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.DATE_MODIFIED,
MediaStore.Images.Media.DATE_TAKEN
};
android.database.Cursor cursor = getContentResolver().query(
imageUrls.get(currentPosition), projection, null, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
int dateAddedCol = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED);
int dateTakenCol = cursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN);
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
if (dateTakenCol >= 0) {
long dateTaken = cursor.getLong(dateTakenCol);
if (dateTaken > 0) {
android.widget.TextView labelTaken = new android.widget.TextView(this);
labelTaken.setText("Date Taken:");
labelTaken.setTextColor(getColor(android.R.color.darker_gray));
labelTaken.setTextSize(14);
labelTaken.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelTaken);
android.widget.TextView valueTaken = new android.widget.TextView(this);
valueTaken.setText(sdf.format(new java.util.Date(dateTaken)));
valueTaken.setTextColor(getColor(android.R.color.black));
valueTaken.setTextSize(14);
valueTaken.setTextIsSelectable(true);
layout.addView(valueTaken);
}
}
if (dateAddedCol >= 0) {
long dateAdded = cursor.getLong(dateAddedCol);
if (dateAdded > 0) {
android.widget.TextView labelAdded = new android.widget.TextView(this);
labelAdded.setText("Date Added:");
labelAdded.setTextColor(getColor(android.R.color.darker_gray));
labelAdded.setTextSize(14);
labelAdded.setTypeface(null, android.graphics.Typeface.BOLD);
layout.addView(labelAdded);
android.widget.TextView valueAdded = new android.widget.TextView(this);
valueAdded.setText(sdf.format(new java.util.Date(dateAdded * 1000)));
valueAdded.setTextColor(getColor(android.R.color.black));
valueAdded.setTextSize(14);
valueAdded.setTextIsSelectable(true);
layout.addView(valueAdded);
}
}
}
cursor.close();
}
} catch (Exception e) {
LogUtils.e(TAG, "get date error: " + e.getMessage());
}
new AlertDialog.Builder(this)
.setTitle("Image Info")
.setView(layout)
.setPositiveButton("OK", null)
.show();
}
@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();
}
}
@@ -1,407 +0,0 @@
package cc.winboll.studio.gallery;
import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.provider.MediaStore;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
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.gallery.App;
import cc.winboll.studio.gallery.utils.BackgroundUtils;
import cc.winboll.studio.libappbase.LogActivity;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.ToastUtils;
import com.a4455jkjh.colorpicker.ColorPickerDialog;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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;
private FloatingActionButton fabScrollTop;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Handler mainHandler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LogUtils.d(TAG, "onCreate");
View content = findViewById(android.R.id.content);
if (content != null) {
content.setBackground(BackgroundUtils.getInstance().getDrawable());
}
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();
adapter.setContext(this);
recyclerView.setAdapter(adapter);
fabScrollTop = findViewById(R.id.fab_scroll_top);
fabScrollTop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
recyclerView.scrollToPosition(0);
}
});
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
if (layoutManager != null) {
int firstVisible = layoutManager.findFirstVisibleItemPosition();
if (firstVisible > 0) {
fabScrollTop.setVisibility(View.VISIBLE);
} else {
fabScrollTop.setVisibility(View.GONE);
}
}
}
});
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() {
executor.execute(new Runnable() {
@Override
public void run() {
final ArrayList<Album> albums = loadAlbumsInBackground();
mainHandler.post(new Runnable() {
@Override
public void run() {
if (albums.isEmpty()) {
Toast.makeText(MainActivity.this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
}
adapter.setData(albums);
LogUtils.d(TAG, "Loaded " + albums.size() + " albums");
}
});
}
});
}
private ArrayList<Album> loadAlbumsInBackground() {
LogUtils.d(TAG, "loadAlbumsInBackground");
String folderPath = prefs.getFolderPath();
File baseFolder = new File(folderPath);
if (!baseFolder.exists() || !baseFolder.isDirectory()) {
folderPath = Preferences.getDefaultPath();
baseFolder = new File(folderPath);
if (!baseFolder.exists()) {
folderPath = Environment.getExternalStorageDirectory() + "/Pictures";
baseFolder = new File(folderPath);
}
}
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
PinnedAlbumDbHelper pinnedDbHelper = PinnedAlbumDbHelper.getInstance(this);
Set<String> pinnedPaths = new HashSet<String>();
String[] pinnedArray = pinnedDbHelper.getPinnedPaths();
pinnedPaths.addAll(Arrays.asList(pinnedArray));
ArrayList<Album> albums = new ArrayList<Album>();
File[] subfolders = baseFolder.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isDirectory();
}
});
if (subfolders != null) {
for (File subfolder : subfolders) {
String albumPath = subfolder.getAbsolutePath();
String coverPath = coverDbHelper.getCover(albumPath);
Uri coverUri = null;
if (coverPath != null) {
File coverFile = new File(coverPath);
if (coverFile.exists()) {
coverUri = Uri.fromFile(coverFile);
} else {
coverUri = getUriFromPath(coverPath);
}
}
ArrayList<Uri> allImages = getImagesInFolder(albumPath);
if (coverUri == null && !allImages.isEmpty()) {
coverUri = allImages.get(0);
}
if (coverUri != null || !allImages.isEmpty()) {
int imageCount = allImages.size();
albums.add(new Album(subfolder.getName(), albumPath, coverUri, imageCount));
}
}
}
ArrayList<Album> pinned = new ArrayList<Album>();
ArrayList<Album> unpinned = new ArrayList<Album>();
for (Album album : albums) {
if (pinnedPaths.contains(album.getPath())) {
pinned.add(album);
} else {
unpinned.add(album);
}
}
pinned.addAll(unpinned);
return pinned;
}
private Uri getUriFromPath(String path) {
String[] projection = { MediaStore.Images.Media._ID };
String selection = MediaStore.Images.Media.DATA + " = ?";
String[] selectionArgs = { path };
Cursor cursor = getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection, selection, selectionArgs, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
return Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
}
} finally {
cursor.close();
}
}
return null;
}
private ArrayList<Uri> getImagesInFolder(String folderPath) {
ArrayList<Uri> imageUrls = new ArrayList<Uri>();
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";
Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder);
if (cursor != null) {
try {
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);
}
}
} finally {
cursor.close();
}
}
return imageUrls;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
MenuItem debugItem = menu.findItem(R.id.action_debug);
if (debugItem != null) {
debugItem.setVisible(App.isDebugging());
}
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_mi_gallery) {
Toast.makeText(this, "Gallery clicked", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_APP_GALLERY);
startActivity(intent);
return true;
} else if (id == R.id.action_change_bg_color) {
if (BackgroundUtils.DrawableType.COLOR == BackgroundUtils.getInstance().getDrawableType()) {
ColorPickerDialog dlg = new ColorPickerDialog(this, BackgroundUtils.getInstance().getColor());
dlg.setOnColorChangedListener(new com.a4455jkjh.colorpicker.view.OnColorChangedListener() {
@Override
public void beforeColorChanged() {
}
@Override
public void onColorChanged(int color) {
BackgroundUtils.getInstance().initFromColor(MainActivity.this, color);
View content = findViewById(android.R.id.content);
if (content != null) {
content.setBackground(BackgroundUtils.getInstance().getDrawable());
}
}
@Override
public void afterColorChanged() {
}
});
dlg.show();
}
return true;
} else if (id == R.id.action_settings) {
startActivity(new Intent(this, SettingsActivity.class));
return true;
} else if (id == R.id.action_about) {
startActivity(new Intent(this, AboutActivity.class));
return true;
} else if (id == R.id.action_trash) {
startActivity(new Intent(this, TrashActivity.class));
return true;
} else if (id == R.id.action_debug) {
LogActivity.startLogActivity(this);
return true;
}
return super.onOptionsItemSelected(item);
}
private BroadcastReceiver coverUpdatedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Preferences.ACTION_COVER_UPDATED.equals(intent.getAction())) {
loadAlbums();
}
}
};
@Override
protected void onResume() {
super.onResume();
registerReceiver(coverUpdatedReceiver, new IntentFilter(Preferences.ACTION_COVER_UPDATED));
if (checkPermission()) {
loadAlbums();
}
if (adapter != null) {
adapter.refreshBg();
adapter.refreshPinned();
adapter.refreshCover();
}
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(coverUpdatedReceiver);
}
@Override
protected void onDestroy() {
super.onDestroy();
executor.shutdownNow();
}
}
@@ -1,78 +0,0 @@
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 cc.winboll.studio.libappbase.LogUtils;
public class PinnedAlbumDbHelper extends SQLiteOpenHelper {
public static final String TAG = "PinnedAlbumDbHelper";
private static final String DB_NAME = "pinned_album.db";
private static final int DB_VERSION = 1;
private static final String TABLE_NAME = "pinned_albums";
private static final String COLUMN_PATH = "album_path";
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_PATH + " TEXT PRIMARY KEY)";
private static PinnedAlbumDbHelper dbHelper;
public static PinnedAlbumDbHelper getInstance(Context context) {
if (dbHelper == null) {
dbHelper = new PinnedAlbumDbHelper(context.getApplicationContext());
}
return dbHelper;
}
public PinnedAlbumDbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
public void pinAlbum(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_PATH, albumPath);
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
LogUtils.d(TAG, "pinAlbum: " + albumPath);
}
public void unpinAlbum(String albumPath) {
SQLiteDatabase db = getWritableDatabase();
db.delete(TABLE_NAME, COLUMN_PATH + " = ?", new String[]{albumPath});
LogUtils.d(TAG, "unpinAlbum: " + albumPath);
}
public boolean isPinned(String albumPath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, null, COLUMN_PATH + " = ?",
new String[]{albumPath}, null, null, null);
boolean pinned = cursor.getCount() > 0;
cursor.close();
return pinned;
}
public String[] getPinnedPaths() {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH}, null, null, null, null, null);
String[] paths = new String[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
paths[i++] = cursor.getString(0);
}
cursor.close();
return paths;
}
}
@@ -1,78 +0,0 @@
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 cc.winboll.studio.libappbase.LogUtils;
public class PinnedImageDbHelper extends SQLiteOpenHelper {
public static final String TAG = "PinnedImageDbHelper";
private static final String DB_NAME = "pinned_image.db";
private static final int DB_VERSION = 1;
private static final String TABLE_NAME = "pinned_images";
private static final String COLUMN_PATH = "image_path";
private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_PATH + " TEXT PRIMARY KEY)";
private static PinnedImageDbHelper dbHelper;
public static PinnedImageDbHelper getInstance(Context context) {
if (dbHelper == null) {
dbHelper = new PinnedImageDbHelper(context.getApplicationContext());
}
return dbHelper;
}
public PinnedImageDbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
public void pinImage(String imagePath) {
SQLiteDatabase db = getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_PATH, imagePath);
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
LogUtils.d(TAG, "pinImage: " + imagePath);
}
public void unpinImage(String imagePath) {
SQLiteDatabase db = getWritableDatabase();
db.delete(TABLE_NAME, COLUMN_PATH + " = ?", new String[]{imagePath});
LogUtils.d(TAG, "unpinImage: " + imagePath);
}
public boolean isPinned(String imagePath) {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, null, COLUMN_PATH + " = ?",
new String[]{imagePath}, null, null, null);
boolean pinned = cursor.getCount() > 0;
cursor.close();
return pinned;
}
public String[] getPinnedPaths() {
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, new String[]{COLUMN_PATH}, null, null, null, null, null);
String[] paths = new String[cursor.getCount()];
int i = 0;
while (cursor.moveToNext()) {
paths[i++] = cursor.getString(0);
}
cursor.close();
return paths;
}
}
@@ -1,93 +0,0 @@
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 KEY_BG_TYPE = "bg_type";
private static final String KEY_ALBUM_SORT_MODE = "album_sort_mode";
private static final String KEY_COVER_WIDTH = "cover_width";
private static final String KEY_COVER_HEIGHT = "cover_height";
private static final String KEY_COVER_RATIO = "cover_ratio";
public static final String ACTION_COVER_UPDATED = "cc.winboll.studio.gallery.COVER_UPDATED";
private static final int DEFAULT_BG_TYPE = 0;
private static final int DEFAULT_SORT_MODE = 0;
private static final int DEFAULT_COVER_WIDTH = 240;
private static final int DEFAULT_COVER_HEIGHT = 120;
private static final float DEFAULT_COVER_RATIO = 2.0f;
private static final String DEFAULT_PATH = "/storage/emulated/0/Pictures/Gallery/owner";
public static final int SORT_TIME_DESC = 0;
public static final int SORT_TIME_ASC = 1;
public static final int SORT_NAME_DESC = 2;
public static final int SORT_NAME_ASC = 3;
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();
}
public int getBgType() {
return prefs.getInt(KEY_BG_TYPE, DEFAULT_BG_TYPE);
}
public void setBgType(int type) {
LogUtils.d(TAG, "setBgType: " + type);
prefs.edit().putInt(KEY_BG_TYPE, type).apply();
}
public int getAlbumSortMode() {
return prefs.getInt(KEY_ALBUM_SORT_MODE, DEFAULT_SORT_MODE);
}
public void setAlbumSortMode(int mode) {
LogUtils.d(TAG, "setAlbumSortMode: " + mode);
prefs.edit().putInt(KEY_ALBUM_SORT_MODE, mode).apply();
}
public int getCoverWidth() {
return prefs.getInt(KEY_COVER_WIDTH, DEFAULT_COVER_WIDTH);
}
public void setCoverWidth(int width) {
prefs.edit().putInt(KEY_COVER_WIDTH, width).apply();
}
public int getCoverHeight() {
return prefs.getInt(KEY_COVER_HEIGHT, DEFAULT_COVER_HEIGHT);
}
public void setCoverHeight(int height) {
prefs.edit().putInt(KEY_COVER_HEIGHT, height).apply();
}
public float getCoverRatio() {
return prefs.getFloat(KEY_COVER_RATIO, DEFAULT_COVER_RATIO);
}
public void setCoverRatio(float ratio) {
prefs.edit().putFloat(KEY_COVER_RATIO, ratio).apply();
}
}
@@ -1,46 +0,0 @@
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();
}
});
}
}
@@ -1,193 +0,0 @@
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<TrashItem> items = new ArrayList<TrashItem>();
ArrayList<Uri> uris = new ArrayList<Uri>();
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;
}
}
@@ -1,111 +0,0 @@
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<TrashAdapter.ViewHolder> {
public static final String TAG = "TrashAdapter";
private ArrayList<TrashActivity.TrashItem> trashItems = new ArrayList<TrashActivity.TrashItem>();
private ArrayList<Uri> imageUrls = new ArrayList<Uri>();
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<TrashActivity.TrashItem> items, ArrayList<Uri> 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 final 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);
}
}
}
@@ -1,78 +0,0 @@
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();
}
}
@@ -1,158 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
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);
LogUtils.d(TAG, "trashFile exists: " + trashFile.exists() + ", path: " + trashFile.getAbsolutePath());
if (!trashFile.exists()) {
LogUtils.e(TAG, "trashFile not exists: " + trashFile.getAbsolutePath());
return false;
}
File originalFolder = new File(originalPath).getParentFile();
LogUtils.d(TAG, "originalFolder: " + originalFolder + ", exists: " + (originalFolder != null && originalFolder.exists()));
if (originalFolder != null && !originalFolder.exists()) {
boolean created = originalFolder.mkdirs();
LogUtils.d(TAG, "mkdirs result: " + created + ", path: " + originalFolder.getAbsolutePath());
}
File originalFile = new File(originalPath);
String restoreName = originalFile.getName();
File restoreFile = new File(originalFolder, restoreName);
LogUtils.d(TAG, "restoreFile: " + restoreFile.getAbsolutePath() + ", exists: " + restoreFile.exists());
boolean renameResult = trashFile.renameTo(restoreFile);
LogUtils.d(TAG, "renameTo result: " + renameResult);
if (renameResult) {
dbHelper.delete(id);
LogUtils.i(TAG, "Restored: " + fileName);
scanMedia(restoreFile.getAbsolutePath());
return true;
}
// Try copy + delete if rename failed
LogUtils.i(TAG, "renameTo failed, trying copy + delete");
try {
java.io.InputStream in = new java.io.FileInputStream(trashFile);
java.io.OutputStream out = new java.io.FileOutputStream(restoreFile);
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
in.close();
out.close();
boolean deleted = trashFile.delete();
LogUtils.d(TAG, "copy+delete result: " + deleted);
if (deleted) {
dbHelper.delete(id);
LogUtils.i(TAG, "Restored (copy): " + fileName);
scanMedia(restoreFile.getAbsolutePath());
return true;
}
} catch (Exception e) {
LogUtils.e(TAG, "copy failed: " + e.getMessage());
}
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";
}
private void scanMedia(String filePath) {
LogUtils.d(TAG, "scanMedia: " + filePath);
MediaScannerConnection.scanFile(context, new String[]{filePath}, null, new android.media.MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
LogUtils.d(TAG, "scanCompleted: " + path + " -> " + uri);
}
});
}
}
@@ -1,169 +0,0 @@
package cc.winboll.studio.gallery;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.FrameLayout;
public class ZoomContainerView extends FrameLayout {
private float scaleFactor = 1.0f;
private float minScale = 0.1f;
private float maxScale = 5.0f;
private static final float ZOOM_STEP = 0.25f;
private ScaleGestureDetector scaleGestureDetector;
private GestureDetector gestureDetector;
private Paint borderPaint;
public ZoomContainerView(Context context) {
super(context);
init();
}
public ZoomContainerView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ZoomContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setWillNotDraw(false);
setBackgroundColor(Color.YELLOW);
borderPaint = new Paint();
borderPaint.setColor(Color.parseColor("#333333"));
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(2);
scaleGestureDetector = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
scaleFactor = Math.max(minScale, Math.min(scaleFactor, maxScale));
invalidate();
requestLayout();
return true;
}
});
gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
if (scaleFactor > 1.5f) {
scaleFactor = 1.0f;
} else {
scaleFactor = 2.0f;
}
invalidate();
requestLayout();
return true;
}
});
}
public void zoomIn() {
scaleFactor = Math.min(maxScale, scaleFactor + ZOOM_STEP);
requestMeasure();
}
public void zoomOut() {
scaleFactor = Math.max(minScale, scaleFactor - ZOOM_STEP);
requestMeasure();
}
private void requestMeasure() {
removeCallbacks(measureRunable);
post(measureRunable);
}
private Runnable measureRunable = new Runnable() {
@Override
public void run() {
requestLayout();
invalidate();
}
};
public float getScaleFactor() {
return scaleFactor;
}
public void setScaleFactor(float scale) {
scaleFactor = Math.max(minScale, Math.min(scale, maxScale));
int childCount = getChildCount();
if (childCount > 0) {
View child = getChildAt(0);
if (child instanceof CropCanvasView) {
((CropCanvasView) child).setContainerScale(scaleFactor);
}
}
requestLayout();
invalidate();
}
public void resetZoom() {
scaleFactor = 1.0f;
invalidate();
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
if (childCount > 0) {
View child = getChildAt(0);
child.measure(widthMeasureSpec, heightMeasureSpec);
int childW = child.getMeasuredWidth();
int childH = child.getMeasuredHeight();
int scaledW = (int) (childW * scaleFactor);
int scaledH = (int) (childH * scaleFactor);
int widthSpec = MeasureSpec.makeMeasureSpec(scaledW, MeasureSpec.EXACTLY);
int heightSpec = MeasureSpec.makeMeasureSpec(scaledH, MeasureSpec.EXACTLY);
child.measure(widthSpec, heightSpec);
setMeasuredDimension(scaledW, scaledH);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int childCount = getChildCount();
if (childCount > 0) {
View child = getChildAt(0);
int childW = child.getMeasuredWidth();
int childH = child.getMeasuredHeight();
int scaledW = (int) (childW * scaleFactor);
int scaledH = (int) (childH * scaleFactor);
int parentW = right - left;
int parentH = bottom - top;
int childLeft = (parentW - scaledW) / 2;
int childTop = (parentH - scaledH) / 2;
child.layout(childLeft, childTop, childLeft + scaledW, childTop + scaledH);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean handled = scaleGestureDetector.onTouchEvent(event);
if (!scaleGestureDetector.isInProgress()) {
handled = gestureDetector.onTouchEvent(event) || handled;
}
return handled || super.onTouchEvent(event);
}
}
@@ -1,44 +0,0 @@
package cc.winboll.studio.gallery.dialog;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.WindowManager;
import android.widget.TextView;
import androidx.annotation.NonNull;
import cc.winboll.studio.gallery.R;
/**
* 颜色表对话框
* 继承于普通对话框类,使用视图文件
*/
public class ColorPaletteDialog extends Dialog {
public ColorPaletteDialog(@NonNull Context context) {
super(context, R.style.ColorPaletteDialog);
}
public ColorPaletteDialog(@NonNull Context context, int themeResId) {
super(context, themeResId);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// super.onCreate(savedInstanceState);
setContentView(R.layout.dialog_color_palette);
TextView titleText = findViewById(R.id.title_text);
WindowManager.LayoutParams params = getWindow().getAttributes();
params.width = WindowManager.LayoutParams.MATCH_PARENT;
getWindow().setAttributes(params);
}
public void setTitle(String title) {
}
public interface OnColorItemClick {
void onColorClick(int color);
}
}
@@ -1,129 +0,0 @@
package cc.winboll.studio.gallery.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.core.content.ContextCompat;
public class BackgroundUtils {
public enum DrawableType {
RESOURCE_ID,
COLOR
}
private static volatile BackgroundUtils instance;
private static final String PREF_NAME = "background_prefs";
private static final String KEY_TYPE = "bg_type";
private static final String KEY_RES_ID = "bg_res_id";
private static final String KEY_COLOR = "bg_color";
private Context context;
private Drawable drawable;
private DrawableType drawableType;
private int resId;
private int color;
private BackgroundUtils() {
}
public static BackgroundUtils getInstance() {
if (instance == null) {
synchronized (BackgroundUtils.class) {
if (instance == null) {
instance = new BackgroundUtils();
}
}
}
return instance;
}
public static BackgroundUtils initFromResource(Context context, @DrawableRes int resId) {
synchronized (BackgroundUtils.class) {
BackgroundUtils utils = getInstance();
utils.context = context.getApplicationContext();
utils.drawableType = DrawableType.RESOURCE_ID;
utils.resId = resId;
utils.drawable = ContextCompat.getDrawable(utils.context, resId);
utils.saveToPreferences();
return utils;
}
}
public static BackgroundUtils initFromColor(Context context, @ColorInt int color) {
synchronized (BackgroundUtils.class) {
BackgroundUtils utils = getInstance();
utils.context = context.getApplicationContext();
utils.drawableType = DrawableType.COLOR;
utils.color = color;
utils.drawable = new ColorDrawable(color);
utils.saveToPreferences();
return utils;
}
}
public static BackgroundUtils initFromPreferences(Context context) {
synchronized (BackgroundUtils.class) {
Context appContext = context.getApplicationContext();
SharedPreferences prefs = appContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
int type = prefs.getInt(KEY_TYPE, -1);
if (type == 0) {
int resId = prefs.getInt(KEY_RES_ID, 0);
if (resId != 0) {
return initFromResource(appContext, resId);
}
} else if (type == 1) {
int color = prefs.getInt(KEY_COLOR, Color.BLACK);
return initFromColor(appContext, color);
}
// 默认情况,initFromColor 内部已经调用了 saveToPreferences()
return initFromColor(appContext, 0xFF00FF00);
}
}
public Drawable getDrawable() {
return drawable;
}
public DrawableType getDrawableType() {
return drawableType;
}
public DrawableType getAttributeValueType() {
return drawableType;
}
public int getResId() {
return resId;
}
public int getColor() {
return color;
}
public void saveToPreferences() {
if (context == null) return;
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
if (drawableType == DrawableType.RESOURCE_ID) {
editor.putInt(KEY_TYPE, 0);
editor.putInt(KEY_RES_ID, resId);
editor.remove(KEY_COLOR);
} else {
editor.putInt(KEY_TYPE, 1);
editor.putInt(KEY_COLOR, color);
editor.remove(KEY_RES_ID);
}
editor.apply();
}
public static void clearPreferences(Context context) {
SharedPreferences prefs = context.getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
prefs.edit().clear().apply();
}
}
@@ -1,124 +0,0 @@
package cc.winboll.studio.gallery.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.core.content.ContextCompat;
public class CropBackgroundUtils {
public enum DrawableType {
RESOURCE_ID,
COLOR
}
private static volatile CropBackgroundUtils instance;
private static final String PREF_NAME = "crop_background_prefs";
private static final String KEY_TYPE = "crop_bg_type";
private static final String KEY_RES_ID = "crop_bg_res_id";
private static final String KEY_COLOR = "crop_bg_color";
private Context context;
private Drawable drawable;
private DrawableType drawableType;
private int resId;
private int color;
private CropBackgroundUtils() {
}
public static CropBackgroundUtils getInstance() {
if (instance == null) {
synchronized (CropBackgroundUtils.class) {
if (instance == null) {
instance = new CropBackgroundUtils();
}
}
}
return instance;
}
public static CropBackgroundUtils initFromResource(Context context, @DrawableRes int resId) {
synchronized (CropBackgroundUtils.class) {
CropBackgroundUtils utils = getInstance();
utils.context = context.getApplicationContext();
utils.drawableType = DrawableType.RESOURCE_ID;
utils.resId = resId;
utils.drawable = ContextCompat.getDrawable(utils.context, resId);
utils.saveToPreferences();
return utils;
}
}
public static CropBackgroundUtils initFromColor(Context context, @ColorInt int color) {
synchronized (CropBackgroundUtils.class) {
CropBackgroundUtils utils = getInstance();
utils.context = context.getApplicationContext();
utils.drawableType = DrawableType.COLOR;
utils.color = color;
utils.drawable = new ColorDrawable(color);
utils.saveToPreferences();
return utils;
}
}
public static CropBackgroundUtils initFromPreferences(Context context) {
synchronized (CropBackgroundUtils.class) {
Context appContext = context.getApplicationContext();
SharedPreferences prefs = appContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
int type = prefs.getInt(KEY_TYPE, -1);
if (type == 0) {
int resId = prefs.getInt(KEY_RES_ID, 0);
if (resId != 0) {
return initFromResource(appContext, resId);
}
} else if (type == 1) {
int color = prefs.getInt(KEY_COLOR, Color.BLACK);
return initFromColor(appContext, color);
}
return initFromColor(appContext, 0xFF00FF00);
}
}
public Drawable getDrawable() {
return drawable;
}
public DrawableType getDrawableType() {
return drawableType;
}
public int getResId() {
return resId;
}
public int getColor() {
return color;
}
public void saveToPreferences() {
if (context == null) return;
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
if (drawableType == DrawableType.RESOURCE_ID) {
editor.putInt(KEY_TYPE, 0);
editor.putInt(KEY_RES_ID, resId);
editor.remove(KEY_COLOR);
} else {
editor.putInt(KEY_TYPE, 1);
editor.putInt(KEY_COLOR, color);
editor.remove(KEY_RES_ID);
}
editor.apply();
}
public static void clearPreferences(Context context) {
SharedPreferences prefs = context.getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
prefs.edit().clear().apply();
}
}
@@ -1,34 +0,0 @@
package cc.winboll.studio.gallery.views;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.RadioButton;
public class BackgroundRadioButton extends RadioButton {
CustomApplicationBackground mCustomApplicationBackground;
public BackgroundRadioButton(Context context) {
super(context);
}
public BackgroundRadioButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BackgroundRadioButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setupCustomApplicationBackground(Context context, int resId) {
mCustomApplicationBackground = new CustomApplicationBackground(context, resId);
}
public void setCustomApplicationBackground(CustomApplicationBackground customApplicationBackground) {
mCustomApplicationBackground = customApplicationBackground;
}
public CustomApplicationBackground getCustomApplicationBackground() {
return mCustomApplicationBackground;
}
}
@@ -1,29 +0,0 @@
package cc.winboll.studio.gallery.views;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
public class CustomApplicationBackground {
Drawable mDrawable;
public CustomApplicationBackground(Drawable drawable) {
mDrawable = drawable;
}
public CustomApplicationBackground(int color) {
mDrawable = new ColorDrawable(color);
}
public CustomApplicationBackground(Context context, int resId) {
mDrawable = context.getDrawable(resId);
}
public Drawable getDrawable() {
return mDrawable;
}
public void setDrawable(Drawable drawable) {
mDrawable = drawable;
}
}
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#000000"/>
</shape>
@@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="10dp"
android:viewportWidth="10"
android:viewportHeight="10">
<path
android:fillColor="#808080"
android:pathData="M0,0h1v1h-1z M2,0h1v1h-1z M4,0h1v1h-1z M6,0h1v1h-1z M8,0h1v1h-1z
M1,1h1v1h-1z M3,1h1v1h-1z M5,1h1v1h-1z M7,1h1v1h-1z M9,1h1v1h-1z
M0,2h1v1h-1z M2,2h1v1h-1z M4,2h1v1h-1z M6,2h1v1h-1z M8,2h1v1h-1z
M1,3h1v1h-1z M3,3h1v1h-1z M5,3h1v1h-1z M7,3h1v1h-1z M9,3h1v1h-1z
M0,4h1v1h-1z M2,4h1v1h-1z M4,4h1v1h-1z M6,4h1v1h-1z M8,4h1v1h-1z
M1,5h1v1h-1z M3,5h1v1h-1z M5,5h1v1h-1z M7,5h1v1h-1z M9,5h1v1h-1z
M0,6h1v1h-1z M2,6h1v1h-1z M4,6h1v1h-1z M6,6h1v1h-1z M8,6h1v1h-1z
M1,7h1v1h-1z M3,7h1v1h-1z M5,7h1v1h-1z M7,7h1v1h-1z M9,7h1v1h-1z
M0,8h1v1h-1z M2,8h1v1h-1z M4,8h1v1h-1z M6,8h1v1h-1z M8,8h1v1h-1z
M1,9h1v1h-1z M3,9h1v1h-1z M5,9h1v1h-1z M7,9h1v1h-1z M9,9h1v1h-1z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M1,0h1v1h-1z M3,0h1v1h-1z M5,0h1v1h-1z M7,0h1v1h-1z M9,0h1v1h-1z
M0,1h1v1h-1z M2,1h1v1h-1z M4,1h1v1h-1z M6,1h1v1h-1z M8,1h1v1h-1z
M1,2h1v1h-1z M3,2h1v1h-1z M5,2h1v1h-1z M7,2h1v1h-1z M9,2h1v1h-1z
M0,3h1v1h-1z M2,3h1v1h-1z M4,3h1v1h-1z M6,3h1v1h-1z M8,3h1v1h-1z
M1,4h1v1h-1z M3,4h1v1h-1z M5,4h1v1h-1z M7,4h1v1h-1z M9,4h1v1h-1z
M0,5h1v1h-1z M2,5h1v1h-1z M4,5h1v1h-1z M6,5h1v1h-1z M8,5h1v1h-1z
M1,6h1v1h-1z M3,6h1v1h-1z M5,6h1v1h-1z M7,6h1v1h-1z M9,6h1v1h-1z
M0,7h1v1h-1z M2,7h1v1h-1z M4,7h1v1h-1z M6,7h1v1h-1z M8,7h1v1h-1z
M1,8h1v1h-1z M3,8h1v1h-1z M5,8h1v1h-1z M7,8h1v1h-1z M9,8h1v1h-1z
M0,9h1v1h-1z M2,9h1v1h-1z M4,9h1v1h-1z M6,9h1v1h-1z M8,9h1v1h-1z"/>
</vector>
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#CCFFFFFF"/>
<stroke
android:width="1dp"
android:color="#000000"/>
</shape>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/transparent"/>
<stroke android:width="2dp" android:color="@android:color/white"/>
<size android:width="32dp" android:height="32dp"/>
</shape>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke android:width="2dp" android:color="@android:color/white"/>
<size android:width="36dp" android:height="36dp"/>
</shape>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/black"/>
<stroke android:width="2dp" android:color="@android:color/white"/>
</shape>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF"/>
</shape>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z"/>
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>
-12
View File
@@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M4,4h7v7h-7z M13,4h7v7h-7z M4,13h7v7h-7z M13,13h7v7h-7z"/>
<path
android:fillColor="#808080"
android:pathData="M11,4h2v7h-2z M4,11h7v2h-7z M13,11h7v2h-7z M11,13h2v7h-2z M13,4h2v7h-2z M4,13h7v2h-7z"/>
</vector>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M20.71,5.63l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-3.12,3.12 -1.93,-1.91 -1.41,1.41 1.42,1.42L3,16.25V21h4.75l8.92,-8.92 1.42,1.42 1.41,-1.41 -1.92,-1.92 3.12,-3.12c0.4,-0.4 0.4,-1.03 0.01,-1.42zM6.92,19H5v-1.92l8.06,-8.06 1.92,1.92L6.92,19z"/>
</vector>
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M21,3H3C1.9,3 1,3.9 1,5v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V5C23,3.9 22.1,3 21,3zM21,19H3V5h18V19z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M9,12l2,2l4,-4l1.5,1.5L11,17l-3,-3z"/>
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,-1H5v2h14V4z"/>
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8s-3.59,8 -8,8z"/>
<path
android:fillColor="#000000"
android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5s-2.24,-5 -5,-5zM12,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3s3,1.34 3,3s-1.34,3 -3,3z"/>
<path
android:fillColor="#000000"
android:pathData="M8,12l1.5,-2l2,2.5l2.5,-3l2,2.5"/>
</vector>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:clickable="true">
<item
android:left="0dp"
android:top="0dp"
android:right="0dp"
android:bottom="0dp"
android:drawable="@drawable/mi"/>
</layer-list>

Some files were not shown because too many files have changed in this diff Show More