Compare commits
26 Commits
gallery-v1
...
positions-
| Author | SHA1 | Date | |
|---|---|---|---|
| c509a0126c | |||
| 07a7409d10 | |||
| f1a7313ac1 | |||
| 41492a2251 | |||
| b5431ccac2 | |||
| ee3e202ecf | |||
| 5e7828cf2b | |||
| 4c5df10c54 | |||
| c0084cd160 | |||
| f89dbede9c | |||
| c946d7af3a | |||
| b8ddd87e66 | |||
| 5ffc049790 | |||
| 0fcf1c5952 | |||
| 6d907e46cb | |||
| 5ab16c2387 | |||
| 160614ce2a | |||
| 1e0a9d222c | |||
| a93cad67a4 | |||
| db264eb85a | |||
| 8361cb0728 | |||
| 92f94f462f | |||
| 22c719d87c | |||
| 94483067cb | |||
| f21b69c64c | |||
| 30123efd4e |
@@ -1,8 +0,0 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sun Apr 26 23:52:51 HKT 2026
|
||||
stageCount=7
|
||||
libraryProject=
|
||||
baseVersion=15.0
|
||||
publishVersion=15.0.6
|
||||
buildCount=0
|
||||
baseBetaVersion=15.0.7
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Put flavor specific strings here -->
|
||||
<string name="app_name">Gallery+</string>
|
||||
</resources>
|
||||
@@ -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/AppTheme"
|
||||
android:resizeableActivity="true"
|
||||
android:name=".GlobalWinBoLLApplication"
|
||||
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("WinBoLL");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
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,255 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
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.Collections;
|
||||
import java.util.Comparator;
|
||||
|
||||
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 = sortAlbums(albums);
|
||||
LogUtils.d(TAG, "setData: " + albums.size() + " albums");
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private ArrayList<Album> sortAlbums(ArrayList<Album> list) {
|
||||
if (pinnedDbHelper == null || list == null || list.isEmpty()) {
|
||||
return list;
|
||||
}
|
||||
ArrayList<Album> pinned = new ArrayList<>();
|
||||
ArrayList<Album> unpinned = new ArrayList<>();
|
||||
for (Album album : list) {
|
||||
if (pinnedDbHelper.isPinned(album.getPath())) {
|
||||
pinned.add(album);
|
||||
} else {
|
||||
unpinned.add(album);
|
||||
}
|
||||
}
|
||||
pinned.addAll(unpinned);
|
||||
return pinned;
|
||||
}
|
||||
|
||||
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()) {
|
||||
ArrayList<Album> pinned = new ArrayList<>();
|
||||
ArrayList<Album> unpinned = new ArrayList<>();
|
||||
for (Album album : albums) {
|
||||
if (pinnedDbHelper.isPinned(album.getPath())) {
|
||||
pinned.add(album);
|
||||
} else {
|
||||
unpinned.add(album);
|
||||
}
|
||||
}
|
||||
Comparator<Album> nameComparator = new Comparator<Album>() {
|
||||
@Override
|
||||
public int compare(Album a1, Album a2) {
|
||||
return a1.getName().compareToIgnoreCase(a2.getName());
|
||||
}
|
||||
};
|
||||
Collections.sort(pinned, nameComparator);
|
||||
Collections.sort(unpinned, nameComparator);
|
||||
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());
|
||||
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 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,170 +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.provider.MediaStore;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
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 Bitmap originalBitmap;
|
||||
private String imagePath;
|
||||
private String albumPath;
|
||||
private int cropWidth = 240;
|
||||
private int cropHeight = 120;
|
||||
private float cropRatio = 2.0f;
|
||||
|
||||
@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);
|
||||
|
||||
Preferences prefs = new Preferences(this);
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
final ImageView btnColorPick = findViewById(R.id.btn_color_pick);
|
||||
btnColorPick.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
boolean pickMode = !cropCanvasView.isColorPickMode();
|
||||
cropCanvasView.setColorPickMode(pickMode);
|
||||
btnColorPick.setAlpha(pickMode ? 0.5f : 1.0f);
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.btn_done).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
saveCroppedCover();
|
||||
}
|
||||
});
|
||||
|
||||
cropCanvasView = findViewById(R.id.crop_canvas_view);
|
||||
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() {
|
||||
Bitmap canvasBitmap = cropCanvasView.getCanvasBitmap();
|
||||
if (canvasBitmap == null) {
|
||||
Toast.makeText(this, "Failed to get image", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
RectF cropRect = cropCanvasView.getCropRect();
|
||||
|
||||
int cropX = (int) cropRect.left;
|
||||
int cropY = (int) cropRect.top;
|
||||
int cropW = (int) cropRect.width();
|
||||
int cropH = (int) cropRect.height();
|
||||
|
||||
cropX = Math.max(0, Math.min(cropX, canvasBitmap.getWidth() - 1));
|
||||
cropY = Math.max(0, Math.min(cropY, canvasBitmap.getHeight() - 1));
|
||||
cropW = Math.max(1, Math.min(cropW, canvasBitmap.getWidth() - cropX));
|
||||
cropH = Math.max(1, Math.min(cropH, canvasBitmap.getHeight() - cropY));
|
||||
|
||||
Bitmap cropped = Bitmap.createBitmap(canvasBitmap, cropX, cropY, cropW, cropH);
|
||||
|
||||
File cacheDir = getCacheDir();
|
||||
File coverFile = new File(cacheDir, "cover_" + System.currentTimeMillis() + ".png");
|
||||
FileOutputStream fos = new FileOutputStream(coverFile);
|
||||
cropped.compress(Bitmap.CompressFormat.PNG, 100, fos);
|
||||
fos.close();
|
||||
|
||||
cropped.recycle();
|
||||
|
||||
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
|
||||
coverDbHelper.setCoverWithCrop(albumPath, imagePath, coverFile.getAbsolutePath());
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (originalBitmap != null) {
|
||||
originalBitmap.recycle();
|
||||
originalBitmap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,400 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
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 CropCanvasView extends View {
|
||||
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 initialSpan;
|
||||
private int backgroundColor = Color.GREEN;
|
||||
private boolean colorPickMode = false;
|
||||
private float pickX, pickY;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public void setImageBitmap(Bitmap bitmap) {
|
||||
this.originalBitmap = bitmap;
|
||||
}
|
||||
|
||||
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(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
createCanvasBitmap();
|
||||
|
||||
requestLayout();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private void createCanvasBitmap() {
|
||||
if (canvasBitmap != null) {
|
||||
canvasBitmap.recycle();
|
||||
}
|
||||
canvasBitmap = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888);
|
||||
android.graphics.Canvas canvas = new android.graphics.Canvas(canvasBitmap);
|
||||
canvas.drawColor(backgroundColor);
|
||||
if (originalBitmap != null && !originalBitmap.isRecycled()) {
|
||||
canvas.drawBitmap(originalBitmap, imageBounds.left, imageBounds.top, imagePaint);
|
||||
}
|
||||
}
|
||||
|
||||
public Bitmap getCanvasBitmap() {
|
||||
return canvasBitmap;
|
||||
}
|
||||
|
||||
public void setBackgroundColor(int color) {
|
||||
this.backgroundColor = color;
|
||||
createCanvasBitmap();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public int getBackgroundColor() {
|
||||
return backgroundColor;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@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 onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (canvasBitmap != null && !canvasBitmap.isRecycled()) {
|
||||
canvas.drawBitmap(canvasBitmap, 0, 0, null);
|
||||
}
|
||||
|
||||
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 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;
|
||||
|
||||
cropRect.set(centerX - newWidth / 2, centerY - newHeight / 2,
|
||||
centerX + newWidth / 2, centerY + newHeight / 2);
|
||||
|
||||
initialSpan = span;
|
||||
invalidate();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
float x = event.getX();
|
||||
float y = event.getY();
|
||||
|
||||
if (colorPickMode) {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
pickX = x;
|
||||
pickY = y;
|
||||
int color = getColorAt(x, y);
|
||||
setBackgroundColor(color);
|
||||
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 >= 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 / 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) {
|
||||
if (canvasBitmap != null && !canvasBitmap.isRecycled()) {
|
||||
int bitmapX = (int) x;
|
||||
int bitmapY = (int) y;
|
||||
if (bitmapX >= 0 && bitmapX < canvasBitmap.getWidth() &&
|
||||
bitmapY >= 0 && bitmapY < canvasBitmap.getHeight()) {
|
||||
return canvasBitmap.getPixel(bitmapX, bitmapY);
|
||||
}
|
||||
}
|
||||
return Color.TRANSPARENT;
|
||||
}
|
||||
}
|
||||
@@ -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,44 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import cc.winboll.studio.libaes.utils.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/04/24 15:23
|
||||
*/
|
||||
public class GlobalWinBoLLApplication extends GlobalApplication {
|
||||
|
||||
public static final String TAG = "GlobalWinBoLLApplication";
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LogUtils.d(TAG, "onCreate");
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
//setIsDebugging(false);
|
||||
|
||||
WinBoLLActivityManager.init(this);
|
||||
|
||||
// 初始化 Toast 框架
|
||||
ToastUtils.init(this);
|
||||
// 设置 Toast 布局样式
|
||||
//ToastUtils.setView(R.layout.view_toast);
|
||||
//ToastUtils.setStyle(new WhiteToastStyle());
|
||||
//ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
|
||||
|
||||
//CrashHandler.getInstance().registerGlobal(this);
|
||||
//CrashHandler.getInstance().registerPart(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
super.onTerminate();
|
||||
LogUtils.d(TAG, "onTerminate");
|
||||
ToastUtils.release();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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(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 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,72 +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;
|
||||
private int bgType;
|
||||
|
||||
public ImagePagerAdapter(ArrayList<Uri> imageUrls, int bgType) {
|
||||
this.imageUrls = imageUrls;
|
||||
this.bgType = bgType;
|
||||
LogUtils.d(TAG, "ImagePagerAdapter created with " + imageUrls.size() + " images, bgType=" + bgType);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@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(getBgRes());
|
||||
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,438 +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 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 btnBg;
|
||||
private int bgType = 0;
|
||||
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);
|
||||
bgType = prefs.getBgType();
|
||||
|
||||
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);
|
||||
btnBg = findViewById(R.id.btn_bg);
|
||||
|
||||
applyBg();
|
||||
|
||||
ImagePagerAdapter adapter = new ImagePagerAdapter(imageUrls, bgType);
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
btnBg.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
switchBg();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void toggleToolbar() {
|
||||
if (toolbar.getVisibility() == View.VISIBLE) {
|
||||
toolbar.setVisibility(View.GONE);
|
||||
} else {
|
||||
toolbar.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyBg() {
|
||||
int bgRes;
|
||||
switch (bgType) {
|
||||
case 0:
|
||||
bgRes = R.drawable.bg_checkerboard;
|
||||
break;
|
||||
case 1:
|
||||
bgRes = R.drawable.bg_white;
|
||||
break;
|
||||
case 2:
|
||||
bgRes = R.drawable.bg_black;
|
||||
break;
|
||||
default:
|
||||
bgRes = R.drawable.bg_checkerboard;
|
||||
}
|
||||
View container = findViewById(R.id.container);
|
||||
if (container != null) {
|
||||
container.setBackgroundResource(bgRes);
|
||||
}
|
||||
}
|
||||
|
||||
private void switchBg() {
|
||||
final String[] bgNames = {"灰白相间", "全白", "全黑"};
|
||||
final int[] bgResources = {R.drawable.bg_checkerboard, R.drawable.bg_white, R.drawable.bg_black};
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("选择背景")
|
||||
.setSingleChoiceItems(bgNames, bgType, new android.content.DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(android.content.DialogInterface dialog, int which) {
|
||||
bgType = which;
|
||||
prefs.setBgType(which);
|
||||
int currentItem = viewPager.getCurrentItem();
|
||||
View container = findViewById(R.id.container);
|
||||
if (container != null) {
|
||||
container.setBackgroundResource(bgResources[which]);
|
||||
}
|
||||
viewPager.setAdapter(new ImagePagerAdapter(imageUrls, bgType));
|
||||
viewPager.setCurrentItem(currentItem);
|
||||
dialog.dismiss();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
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, bgType));
|
||||
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,357 +0,0 @@
|
||||
package cc.winboll.studio.gallery;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.Settings;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.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 cc.winboll.studio.gallery.AlbumAdapter.OnAlbumClickListener;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.LogActivity;
|
||||
import java.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FilenameFilter;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
public static final String TAG = "MainActivity";
|
||||
private static final int PERMISSION_REQUEST_CODE = 100;
|
||||
private static final int MANAGE_PERMISSION_REQUEST_CODE = 101;
|
||||
private RecyclerView recyclerView;
|
||||
private AlbumAdapter adapter;
|
||||
private Preferences prefs;
|
||||
private FloatingActionButton fabScrollTop;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
LogUtils.d(TAG, "onCreate");
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
prefs = new Preferences(this);
|
||||
|
||||
recyclerView = findViewById(R.id.recycler_view);
|
||||
recyclerView.setLayoutManager(new GridLayoutManager(this, 2));
|
||||
adapter = new AlbumAdapter();
|
||||
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() {
|
||||
LogUtils.d(TAG, "loadAlbums");
|
||||
String folderPath = prefs.getFolderPath();
|
||||
File baseFolder = new File(folderPath);
|
||||
LogUtils.d(TAG, "baseFolder: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
|
||||
|
||||
if (!baseFolder.exists() || !baseFolder.isDirectory()) {
|
||||
folderPath = Preferences.getDefaultPath();
|
||||
baseFolder = new File(folderPath);
|
||||
LogUtils.d(TAG, "try default: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
|
||||
if (!baseFolder.exists()) {
|
||||
folderPath = Environment.getExternalStorageDirectory() + "/Pictures";
|
||||
baseFolder = new File(folderPath);
|
||||
LogUtils.d(TAG, "try Pictures: " + baseFolder.getAbsolutePath() + ", exists=" + baseFolder.exists());
|
||||
}
|
||||
}
|
||||
|
||||
AlbumCoverDbHelper coverDbHelper = AlbumCoverDbHelper.getInstance(this);
|
||||
ArrayList<Album> albums = new ArrayList<>();
|
||||
|
||||
FileFilter directoryFilter = new FileFilter() {
|
||||
@Override
|
||||
public boolean accept(File file) {
|
||||
return file.isDirectory();
|
||||
}
|
||||
};
|
||||
File[] subfolders = baseFolder.listFiles(directoryFilter);
|
||||
LogUtils.d(TAG, "subfolders: " + (subfolders != null ? subfolders.length : 0));
|
||||
if (subfolders != null) {
|
||||
for (File subfolder : subfolders) {
|
||||
LogUtils.d(TAG, "scanning folder: " + subfolder.getName());
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (coverUri == null) {
|
||||
ArrayList<Uri> images = getImagesInFolder(albumPath);
|
||||
if (!images.isEmpty()) {
|
||||
coverUri = images.get(0);
|
||||
}
|
||||
}
|
||||
ArrayList<Uri> allImages = getImagesInFolder(albumPath);
|
||||
if (coverUri != null || !allImages.isEmpty()) {
|
||||
if (coverUri == null && !allImages.isEmpty()) {
|
||||
coverUri = allImages.get(0);
|
||||
}
|
||||
int imageCount = allImages.size();
|
||||
albums.add(new Album(subfolder.getName(), albumPath, coverUri, imageCount));
|
||||
LogUtils.d(TAG, "album added: " + subfolder.getName() + ", " + imageCount + " images");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (albums.isEmpty()) {
|
||||
Toast.makeText(this, R.string.no_images_found, Toast.LENGTH_SHORT).show();
|
||||
LogUtils.i(TAG, "No albums found");
|
||||
}
|
||||
adapter.setData(albums);
|
||||
LogUtils.d(TAG, "Loaded " + albums.size() + " albums");
|
||||
}
|
||||
|
||||
private Uri getUriFromPath(String path) {
|
||||
String[] projection = { MediaStore.Images.Media._ID };
|
||||
String selection = MediaStore.Images.Media.DATA + " = ?";
|
||||
String[] selectionArgs = { path };
|
||||
try (Cursor cursor = getContentResolver().query(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
projection, selection, selectionArgs, null)) {
|
||||
if (cursor != null) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ArrayList<Uri> getImagesInFolder(String folderPath) {
|
||||
ArrayList<Uri> imageUrls = new ArrayList<>();
|
||||
ContentResolver contentResolver = getContentResolver();
|
||||
Uri collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
|
||||
String selection = MediaStore.Images.Media.DATA + " LIKE ?";
|
||||
String[] selectionArgs = new String[]{folderPath + "/%"};
|
||||
String sortOrder = MediaStore.Images.Media.DATE_ADDED + " DESC";
|
||||
|
||||
LogUtils.d(TAG, "getImagesInFolder: " + folderPath);
|
||||
|
||||
try (Cursor cursor = contentResolver.query(collection, null, selection, selectionArgs, sortOrder)) {
|
||||
if (cursor != null) {
|
||||
LogUtils.d(TAG, "cursor count: " + cursor.getCount());
|
||||
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));
|
||||
LogUtils.d(TAG, "image: id=" + id + ", path=" + path);
|
||||
imageUrls.add(contentUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "found " + imageUrls.size() + " images");
|
||||
return imageUrls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.action_settings) {
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
return true;
|
||||
} else if (id == R.id.action_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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (checkPermission()) {
|
||||
scanMediaStore();
|
||||
loadAlbums();
|
||||
}
|
||||
if (adapter != null) {
|
||||
adapter.refreshBg();
|
||||
adapter.refreshPinned();
|
||||
adapter.refreshCover();
|
||||
}
|
||||
}
|
||||
|
||||
private void scanMediaStore() {
|
||||
String folderPath = prefs.getFolderPath();
|
||||
File baseFolder = new File(folderPath);
|
||||
if (baseFolder.exists() && baseFolder.isDirectory()) {
|
||||
File[] subfolders = baseFolder.listFiles(new FileFilter() {
|
||||
@Override
|
||||
public boolean accept(File file) {
|
||||
return file.isDirectory();
|
||||
}
|
||||
});
|
||||
if (subfolders != null) {
|
||||
ArrayList<String> paths = new ArrayList<>();
|
||||
for (File subfolder : subfolders) {
|
||||
File[] images = subfolder.listFiles(new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(File dir, String name) {
|
||||
String lower = name.toLowerCase();
|
||||
return lower.endsWith(".jpg") || lower.endsWith(".jpeg")
|
||||
|| lower.endsWith(".png") || lower.endsWith(".gif")
|
||||
|| lower.endsWith(".webp") || lower.endsWith(".bmp");
|
||||
}
|
||||
});
|
||||
if (images != null) {
|
||||
for (File img : images) {
|
||||
paths.add(img.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!paths.isEmpty()) {
|
||||
LogUtils.d(TAG, "scanning " + paths.size() + " files to MediaStore");
|
||||
String[] pathArray = paths.toArray(new String[0]);
|
||||
MediaScannerConnection.scanFile(this, pathArray, null, new MediaScannerConnection.OnScanCompletedListener() {
|
||||
@Override
|
||||
public void onScanCompleted(String path, Uri uri) {
|
||||
LogUtils.d(TAG, "scanCompleted: " + path + " -> " + uri);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,90 +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";
|
||||
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 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,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,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>
|
||||
@@ -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="#000000"
|
||||
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="#000000"
|
||||
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="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,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="M16,12V4H17V2H7V4H8V12L6,14V16H11.5V22H12.5V16H18V14L16,12Z"/>
|
||||
</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="M13,3c-4.97,0 -9,4.03 -9,9H1l3.89,3.89 0.07,0.14L9,12H6c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,17.9 10.51,19 13,19c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08V8H12z"/>
|
||||
</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="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92s2.92,-1.31 2.92,-2.92 -1.31,-2.92 -2.92,-2.92z"/>
|
||||
</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="M3,18h6v-2H3v2zM3,6v2h18V6H3zM3,13h12v-2H3v2z"/>
|
||||
</vector>
|
||||
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<cc.winboll.studio.libappbase.views.AboutView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/aboutview"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,56 +0,0 @@
|
||||
<?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"
|
||||
android:background="@color/black">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:background="@color/colorPrimary"
|
||||
android:paddingHorizontal="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btn_close"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_close"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btn_color_pick"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_color_pick"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btn_done"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:text="完成"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<cc.winboll.studio.gallery.CropCanvasView
|
||||
android:id="@+id/crop_canvas_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,73 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/view_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="false"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:background="#CC000000"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_back"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_back"
|
||||
android:contentDescription="Back"/>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_share"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_share"
|
||||
android:contentDescription="Share"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_info"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_info"
|
||||
android:contentDescription="Info"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_bg"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_bg"
|
||||
android:contentDescription="Background"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_delete"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_delete"
|
||||
android:contentDescription="Delete"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab_scroll_top"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_arrow_up"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/colorAccent"
|
||||
app:tint="@color/white"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,39 +0,0 @@
|
||||
<?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="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/folder_path"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_folder_path"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/enter_folder_path"
|
||||
android:inputType="text"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_current_path"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#888888"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_save"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/save"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,48 +0,0 @@
|
||||
<?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="wrap_content"
|
||||
android:padding="2dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/album_cover"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:layout_margin="2dp"
|
||||
android:scaleType="centerCrop"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="#80000000"
|
||||
android:padding="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pin_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="6dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:padding="3dp"
|
||||
android:src="@drawable/ic_pin"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/image_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|end"
|
||||
android:background="#80000000"
|
||||
android:padding="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="10sp"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,41 +0,0 @@
|
||||
<?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="wrap_content"
|
||||
android:padding="2dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:background="@color/black"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pin_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_margin="4dp"
|
||||
android:background="@drawable/bg_circle_white"
|
||||
android:padding="4dp"
|
||||
android:src="@drawable/ic_pin"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/black"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/cover_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="4dp"
|
||||
android:background="@drawable/bg_circle_white"
|
||||
android:padding="4dp"
|
||||
android:src="@drawable/ic_cover"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/black"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?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">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="fitCenter"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,42 +0,0 @@
|
||||
<?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="wrap_content"
|
||||
android:padding="2dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:background="@color/black"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="#80000000"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btn_restore"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_restore"
|
||||
android:contentDescription="Restore"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btn_delete"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_delete"
|
||||
android:contentDescription="Delete"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,26 +0,0 @@
|
||||
<?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_sort"
|
||||
android:icon="@drawable/ic_sort"
|
||||
android:title="排序"
|
||||
app:showAsAction="ifRoom">
|
||||
<menu>
|
||||
<group android:checkableBehavior="single">
|
||||
<item
|
||||
android:id="@+id/sort_time_desc"
|
||||
android:title="时间倒序"/>
|
||||
<item
|
||||
android:id="@+id/sort_time_asc"
|
||||
android:title="时间正序"/>
|
||||
<item
|
||||
android:id="@+id/sort_name_desc"
|
||||
android:title="名称倒序"/>
|
||||
<item
|
||||
android:id="@+id/sort_name_asc"
|
||||
android:title="名称正序"/>
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
</menu>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?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_done"
|
||||
android:title="完成"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
||||
@@ -1,25 +0,0 @@
|
||||
<?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_trash"
|
||||
android:title="@string/trash"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_settings"
|
||||
android:title="@string/settings"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_about"
|
||||
android:title="@string/about"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_debug"
|
||||
android:title="@string/debug_log"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
</menu>
|
||||
@@ -1,15 +0,0 @@
|
||||
<?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_trash"
|
||||
android:title="@string/trash"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_clear_trash"
|
||||
android:title="@string/clear_trash"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
</menu>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#009688</color>
|
||||
<color name="colorPrimaryDark">#00796B</color>
|
||||
<color name="colorAccent">#FF9800</color>
|
||||
<color name="black">#000000</color>
|
||||
<color name="white">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -1,21 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">Gallery</string>
|
||||
<string name="app_description">WinBoLL Album App</string>
|
||||
<string name="refresh">Refresh</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="about">About</string>
|
||||
<string name="folder_path">Folder Path</string>
|
||||
<string name="enter_folder_path">Enter folder path</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="no_images_found">No images found</string>
|
||||
<string name="trash">Trash</string>
|
||||
<string name="clear_trash">Clear Trash</string>
|
||||
<string name="delete_confirm">Delete to trash?</string>
|
||||
<string name="restore_confirm">Restore to original folder?</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="debug_log">Debug Log</string>
|
||||
<string name="debug_message">Debug log message</string>
|
||||
</resources>
|
||||
@@ -1,11 +0,0 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
1
positions/.gitignore
vendored
Normal file
1
positions/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -1,7 +1,8 @@
|
||||
# Gallery
|
||||
# Positions
|
||||
|
||||
#### 介绍
|
||||
云宝相册应用
|
||||
安卓位置应用,有关于地理位置的相关应用。
|
||||
PS:使用感言~~~『記低用唔到』。
|
||||
|
||||
#### 软件架构
|
||||
适配安卓应用 [AIDE Pro] 的 Gradle 编译结构。
|
||||
@@ -10,7 +11,7 @@
|
||||
|
||||
#### Gradle 编译说明
|
||||
调试版编译命令 :gradle assembleBetaDebug
|
||||
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh gallery
|
||||
阶段版编译命令 :bash .winboll/bashPublishAPKAddTag.sh positions
|
||||
|
||||
#### 使用说明
|
||||
|
||||
@@ -18,43 +18,48 @@ def genVersionName(def versionName){
|
||||
}
|
||||
|
||||
android {
|
||||
// 适配MIUI12
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
// 关键:改为你已安装的 SDK 32(≥ targetSdkVersion 30,兼容已安装环境)
|
||||
compileSdkVersion 32
|
||||
|
||||
// 直接使用已安装的构建工具 33.0.3(无需修改)
|
||||
buildToolsVersion "33.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "cc.winboll.studio.gallery"
|
||||
minSdkVersion 23
|
||||
// 适配MIUI12
|
||||
applicationId "cc.winboll.studio.positions"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
// versionName 更新后需要手动设置
|
||||
// .winboll/winbollBuildProps.properties 文件的 stageCount=0
|
||||
// Gradle编译环境下合起来的 versionName 就是 "${versionName}.0"
|
||||
versionName "15.0"
|
||||
versionName "15.12"
|
||||
if(true) {
|
||||
versionName = genVersionName("${versionName}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 米盟 SDK
|
||||
packagingOptions {
|
||||
doNotStrip "*/*/libmimo_1011.so"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = ['libs'] // 若SO库放在libs目录下
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
api 'com.google.code.gson:gson:2.10.1'
|
||||
// 米盟
|
||||
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
|
||||
//注意:以下5个库必须要引入
|
||||
//api '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'
|
||||
|
||||
// 下拉控件
|
||||
api 'com.baoyz.pullrefreshlayout:library:1.2.0'
|
||||
// https://mvnrepository.com/artifact/com.jzxiang.pickerview/TimePickerDialog
|
||||
api 'com.jzxiang.pickerview:TimePickerDialog:1.0.1'
|
||||
|
||||
// 谷歌定位服务核心依赖(FusedLocationProviderClient所在库)
|
||||
api 'com.google.android.gms:play-services-location:21.0.1'
|
||||
|
||||
// SSH
|
||||
api 'com.jcraft:jsch:0.1.55'
|
||||
@@ -65,55 +70,23 @@ dependencies {
|
||||
api 'com.journeyapps:zxing-android-embedded:3.6.0'
|
||||
// 应用介绍页类库
|
||||
api 'io.github.medyo:android-about-page:2.0.0'
|
||||
// 网络连接类库
|
||||
// 网络连接类库
|
||||
api 'com.squareup.okhttp3:okhttp:4.4.1'
|
||||
// OkHttp网络请求
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
|
||||
// FastJSON解析
|
||||
implementation 'com.alibaba:fastjson:1.2.76'
|
||||
|
||||
// AndroidX 类库
|
||||
/*api 'androidx.appcompat:appcompat:1.1.0'
|
||||
//api 'com.google.android.material:material:1.4.0'
|
||||
// AndroidX 类库
|
||||
api 'androidx.appcompat:appcompat:1.1.0'
|
||||
api 'com.google.android.material:material:1.4.0'
|
||||
//api 'androidx.viewpager:viewpager:1.0.0'
|
||||
//api 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||
//api 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
|
||||
//api 'androidx.fragment:fragment:1.1.0'*/
|
||||
|
||||
|
||||
// 米盟
|
||||
api 'com.miui.zeus:mimo-ad-sdk:5.3.+'//请使用最新版sdk
|
||||
//注意:以下5个库必须要引入
|
||||
//implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
api 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
api 'com.google.code.gson:gson:2.8.5'
|
||||
api 'com.github.bumptech.glide:glide:4.9.0'
|
||||
//annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
|
||||
|
||||
implementation "androidx.annotation:annotation:1.3.0"
|
||||
implementation "androidx.core:core:1.6.0"
|
||||
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
|
||||
implementation "androidx.preference:preference:1.1.1"
|
||||
implementation "androidx.viewpager:viewpager:1.0.0"
|
||||
implementation "com.google.android.material:material:1.4.0"
|
||||
implementation "com.google.guava:guava:24.1-jre"
|
||||
/*
|
||||
implementation "io.noties.markwon:core:$markwonVersion"
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||
implementation "io.noties.markwon:linkify:$markwonVersion"
|
||||
implementation "io.noties.markwon:recycler:$markwonVersion"
|
||||
*/
|
||||
/*implementation 'com.termux:terminal-emulator:0.118.0'
|
||||
implementation 'com.termux:terminal-view:0.118.0'
|
||||
implementation 'com.termux:termux-shared:0.118.0'
|
||||
*/
|
||||
//api 'androidx.fragment:fragment:1.1.0'
|
||||
|
||||
// WinBoLL库 nexus.winboll.cc 地址
|
||||
api 'cc.winboll.studio:libaes:15.15.9'
|
||||
api 'cc.winboll.studio:libappbase:15.15.19'
|
||||
api 'cc.winboll.studio:libaes:15.15.2'
|
||||
api 'cc.winboll.studio:libappbase:15.15.11'
|
||||
|
||||
// WinBoLL备用库 jitpack.io 地址
|
||||
//api 'com.github.ZhanGSKen:AES:aes-v15.15.7'
|
||||
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.15.4'
|
||||
//api 'com.github.ZhanGSKen:AES:aes-v15.12.9'
|
||||
//api 'com.github.ZhanGSKen:APPBase:appbase-v15.14.1'
|
||||
|
||||
api fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
8
positions/build.properties
Normal file
8
positions/build.properties
Normal file
@@ -0,0 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Sat Apr 11 13:00:42 HKT 2026
|
||||
stageCount=19
|
||||
libraryProject=
|
||||
baseVersion=15.12
|
||||
publishVersion=15.12.18
|
||||
buildCount=0
|
||||
baseBetaVersion=15.12.19
|
||||
@@ -67,6 +67,12 @@
|
||||
-keep class okio.** { *; }
|
||||
-dontwarn okhttp3.internal.platform.**
|
||||
-dontwarn okio.**
|
||||
# ============================== 必要补充规则 ==============================
|
||||
# OkHttp 4.4.1 补充规则(Java 7 兼容)
|
||||
-keep class okhttp3.internal.concurrent.** { *; }
|
||||
-keep class okhttp3.internal.connection.** { *; }
|
||||
-dontwarn okhttp3.internal.concurrent.TaskRunner
|
||||
-dontwarn okhttp3.internal.connection.RealCall
|
||||
|
||||
# Glide 4.9.0(米盟广告图片加载依赖)
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
@@ -2,7 +2,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools" >
|
||||
|
||||
<application>
|
||||
<application
|
||||
tools:replace="android:icon"
|
||||
android:icon="@drawable/ic_launcher_beta">
|
||||
|
||||
<!-- Put flavor specific code here -->
|
||||
|
||||
5
positions/src/beta/res/values-zh/strings.xml
Normal file
5
positions/src/beta/res/values-zh/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">悟空笔记#</string>
|
||||
<string name="appplus_name">时空任务#</string>
|
||||
</resources>
|
||||
7
positions/src/beta/res/values/strings.xml
Normal file
7
positions/src/beta/res/values/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name">Positions</string>
|
||||
<string name="appplus_name">PositionsPlus+</string>
|
||||
|
||||
</resources>
|
||||
6
positions/src/beta/res/xml/file_provider.xml
Normal file
6
positions/src/beta/res/xml/file_provider.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-files-path
|
||||
name="BaseBean"
|
||||
path="BaseBean/" />
|
||||
</paths>
|
||||
19
positions/src/beta/res/xml/shortcutsmain.xml
Normal file
19
positions/src/beta/res/xml/shortcutsmain.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 切换启动入口的快捷菜单 -->
|
||||
<shortcut
|
||||
android:shortcutId="open_appplus"
|
||||
android:enabled="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:shortcutShortLabel="@string/open_appplus"
|
||||
android:shortcutLongLabel="@string/open_appplus"
|
||||
android:shortcutDisabledMessage="@string/appplus_open_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.positions.App.ACTION_OPEN_APPPLUS"
|
||||
android:targetPackage="cc.winboll.studio.positions.beta"
|
||||
android:targetClass="cc.winboll.studio.positions.activities.ShortcutActionActivity"
|
||||
android:data="open_appplus" />
|
||||
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
19
positions/src/beta/res/xml/shortcutsplus.xml
Normal file
19
positions/src/beta/res/xml/shortcutsplus.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 切换启动入口的快捷菜单 -->
|
||||
<shortcut
|
||||
android:shortcutId="close_appplus"
|
||||
android:enabled="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:shortcutShortLabel="@string/close_appplus"
|
||||
android:shortcutLongLabel="@string/close_appplus"
|
||||
android:shortcutDisabledMessage="@string/appplus_close_disabled">
|
||||
<intent
|
||||
android:action="cc.winboll.studio.positions.App.ACTION_CLOSE_APPPLUS"
|
||||
android:targetPackage="cc.winboll.studio.positions.beta"
|
||||
android:targetClass="cc.winboll.studio.positions.activities.ShortcutActionActivity"
|
||||
android:data="close_appplus" />
|
||||
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
164
positions/src/main/AndroidManifest.xml
Normal file
164
positions/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,164 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cc.winboll.studio.positions">
|
||||
|
||||
<!-- 只有在前台运行时才能获取大致位置信息 -->
|
||||
<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"/>
|
||||
|
||||
<!-- 运行“location”类型的前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
|
||||
|
||||
<!-- 拥有完全的网络访问权限 -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<!-- 安装快捷方式 -->
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
|
||||
|
||||
<!-- 读取您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 修改或删除您共享存储空间中的内容 -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- 只能在前台获取精确的位置信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/MyAppTheme"
|
||||
android:resizeableActivity="true"
|
||||
android:name=".App">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<action android:name="android.intent.action.EDIT"/>
|
||||
|
||||
<data android:mimeType="application/json"/>
|
||||
|
||||
<data android:mimeType="text/x-json"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityWukong"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:enabled="true">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcutsmain"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityLaojun"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/appplus_name"
|
||||
android:icon="@drawable/ic_positions_plus"
|
||||
android:enabled="false">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcutsplus"/>
|
||||
|
||||
</activity-alias>
|
||||
|
||||
<activity android:name="cc.winboll.studio.positions.activities.LocationActivity"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.positions.activities.ShortcutActionActivity"/>
|
||||
|
||||
<service
|
||||
android:name=".services.MainService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:name=".services.AssistantService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:name=".services.DistanceRefreshService"
|
||||
android:exported="false"/>
|
||||
|
||||
<receiver android:name="cc.winboll.studio.positions.receivers.MotionStatusReceiver">
|
||||
|
||||
<intent-filter>
|
||||
|
||||
<action android:name="cc.winboll.studio.positions.receivers.MotionStatusReceiver"/>
|
||||
|
||||
</intent-filter>
|
||||
|
||||
</receiver>
|
||||
|
||||
<meta-data
|
||||
android:name="android.max_aspect"
|
||||
android:value="4.0"/>
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.version"
|
||||
android:value="@integer/google_play_services_version"/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_provider"/>
|
||||
|
||||
</provider>
|
||||
|
||||
<activity android:name="cc.winboll.studio.positions.activities.SettingsActivity"/>
|
||||
|
||||
<activity android:name="cc.winboll.studio.positions.activities.AboutActivity"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
348
positions/src/main/java/cc/winboll/studio/positions/App.java
Normal file
348
positions/src/main/java/cc/winboll/studio/positions/App.java
Normal file
@@ -0,0 +1,348 @@
|
||||
package cc.winboll.studio.positions;
|
||||
|
||||
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.Gravity;
|
||||
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.WinBoLLActivityManager;
|
||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
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.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() {
|
||||
super.onCreate();
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
|
||||
WinBoLLActivityManager.init(this);
|
||||
|
||||
// 初始化 Toast 框架
|
||||
ToastUtils.init(this);
|
||||
// 设置 Toast 布局样式
|
||||
//ToastUtils.setView(R.layout.view_toast);
|
||||
//ToastUtils.setStyle(new WhiteToastStyle());
|
||||
//ToastUtils.setGravity(Gravity.BOTTOM, 0, 200);
|
||||
|
||||
//CrashHandler.getInstance().registerGlobal(this);
|
||||
//CrashHandler.getInstance().registerPart(this);
|
||||
}
|
||||
|
||||
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,330 @@
|
||||
package cc.winboll.studio.positions;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Switch;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libaes.utils.AESThemeUtil;
|
||||
import cc.winboll.studio.libaes.utils.DevelopUtils;
|
||||
import cc.winboll.studio.libaes.views.ADsBannerView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.positions.R;
|
||||
import cc.winboll.studio.positions.activities.AboutActivity;
|
||||
import cc.winboll.studio.positions.activities.LocationActivity;
|
||||
import cc.winboll.studio.positions.activities.SettingsActivity;
|
||||
import cc.winboll.studio.positions.activities.WinBoLLActivity;
|
||||
import cc.winboll.studio.positions.utils.AppConfigsUtil;
|
||||
import cc.winboll.studio.positions.utils.ServiceUtil;
|
||||
|
||||
/**
|
||||
* 主页面:仅负责
|
||||
* 1. 位置服务启动/停止(通过 Switch 开关控制)
|
||||
* 2. 跳转至“位置管理页(LocationActivity)”和“日志页(LogActivity)”
|
||||
* 3. Java 7 语法适配:无 Lambda、显式接口实现、兼容低版本
|
||||
*/
|
||||
public class MainActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
public static final String TAG = "MainActivity";
|
||||
// 权限请求码(建议定义为类常量,避免魔法值)
|
||||
private static final int REQUEST_LOCATION_PERMISSIONS = 1001;
|
||||
private static final int REQUEST_BACKGROUND_LOCATION_PERMISSION = 1002;
|
||||
|
||||
// UI 控件:服务控制开关、顶部工具栏
|
||||
private Switch mServiceSwitch;
|
||||
private Button mManagePositionsButton;
|
||||
private Toolbar mToolbar;
|
||||
// 服务相关:服务实例、绑定状态标记
|
||||
//private DistanceRefreshService mDistanceService;
|
||||
private boolean isServiceBound = false;
|
||||
ADsBannerView mADsBannerView;
|
||||
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
// ---------------------- 服务连接回调(仅用于获取服务状态,不依赖服务执行核心逻辑) ----------------------
|
||||
// private final ServiceConnection mServiceConn = new ServiceConnection() {
|
||||
// /**
|
||||
// * 服务绑定成功:获取服务实例,同步开关状态(以服务实际状态为准)
|
||||
// */
|
||||
// @Override
|
||||
// public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
// // Java 7 显式强转 Binder 实例(确保类型匹配,避免ClassCastException)
|
||||
// DistanceRefreshService.DistanceBinder binder = (DistanceRefreshService.DistanceBinder) service;
|
||||
// mDistanceService = binder.getService();
|
||||
// isServiceBound = true;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 服务意外断开(如服务崩溃):重置服务实例和绑定状态
|
||||
// */
|
||||
// @Override
|
||||
// public void onServiceDisconnected(ComponentName name) {
|
||||
// mDistanceService = null;
|
||||
// isServiceBound = false;
|
||||
// }
|
||||
// };
|
||||
|
||||
// ---------------------- Activity 生命周期(核心:初始化UI、申请权限、绑定服务、释放资源) ----------------------
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main); // 关联主页面布局
|
||||
|
||||
// 1. 初始化顶部 Toolbar(保留原逻辑,设置页面标题)
|
||||
initToolbar();
|
||||
// 2. 初始化其他控件
|
||||
initViews();
|
||||
// 3. 检查并申请位置权限(含后台GPS权限,确保服务启动前权限就绪)
|
||||
if (!checkLocationPermissions()) {
|
||||
requestLocationPermissions();
|
||||
}
|
||||
// 4. 绑定服务(仅用于获取服务实时状态,不影响服务独立运行)
|
||||
//bindDistanceService();
|
||||
|
||||
mADsBannerView = findViewById(R.id.adsbanner);
|
||||
|
||||
setLLMainBackgroundColor();
|
||||
}
|
||||
|
||||
// 在 Activity 的 onCreate() 或需要获取颜色的方法中调用
|
||||
private void setLLMainBackgroundColor() {
|
||||
// 1. 定义要解析的主题属性(这里是 colorAccent)
|
||||
TypedArray a = getTheme().obtainStyledAttributes(new int[]{android.R.attr.colorAccent});
|
||||
// 2. 获取对应的颜色值(默认值可设为你需要的 fallback 颜色,如 Color.GRAY)
|
||||
int colorAccent = a.getColor(0, Color.GRAY);
|
||||
// 3. 必须回收,避免内存泄漏
|
||||
a.recycle();
|
||||
|
||||
LinearLayout llmain = findViewById(R.id.llmain);
|
||||
llmain.setBackgroundColor(colorAccent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.releaseAdResources();
|
||||
}
|
||||
|
||||
// 页面销毁时解绑服务,避免Activity与服务相互引用导致内存泄漏
|
||||
// if (isServiceBound) {
|
||||
// unbindService(mServiceConn);
|
||||
// isServiceBound = false;
|
||||
// mDistanceService = null;
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (mADsBannerView != null) {
|
||||
mADsBannerView.resumeADs(MainActivity.this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------------------- 核心功能1:初始化UI组件(Toolbar + 服务开关) ----------------------
|
||||
/**
|
||||
* 初始化顶部 Toolbar,设置页面标题
|
||||
*/
|
||||
private void initToolbar() {
|
||||
mToolbar = (Toolbar) findViewById(R.id.toolbar); // Java 7 显式 findViewById + 强转
|
||||
setSupportActionBar(mToolbar);
|
||||
// 给ActionBar设置标题(先判断非空,避免空指针异常)
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setTitle(getString(R.string.app_name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化服务控制开关:读取SP状态、绑定点击事件(含权限检查)
|
||||
*/
|
||||
private void initViews() {
|
||||
mServiceSwitch = (Switch) findViewById(R.id.switch_service_control); // 显式强转
|
||||
mServiceSwitch.setChecked(AppConfigsUtil.getInstance(this).isEnableMainService(true));
|
||||
|
||||
mManagePositionsButton = (Button) findViewById(R.id.btn_manage_positions);
|
||||
mManagePositionsButton.setEnabled(mServiceSwitch.isChecked());
|
||||
|
||||
// Java 7 用匿名内部类实现 CompoundButton.OnCheckedChangeListener
|
||||
mServiceSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
// 开关打开前先检查权限:无权限则终止操作、重置开关、引导申请
|
||||
if (isChecked && !checkLocationPermissions()) {
|
||||
requestLocationPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
// 权限就绪:执行服务启停逻辑
|
||||
if (isChecked) {
|
||||
LogUtils.d(TAG, "设置启动服务");
|
||||
ServiceUtil.startAutoService(MainActivity.this);
|
||||
} else {
|
||||
LogUtils.d(TAG, "设置关闭服务");
|
||||
|
||||
ServiceUtil.stopAutoService(MainActivity.this);
|
||||
}
|
||||
|
||||
mManagePositionsButton.setEnabled(isChecked);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// 主题菜单
|
||||
AESThemeUtil.inflateMenu(this, menu);
|
||||
// 调试工具菜单
|
||||
if (App.isDebugging()) {
|
||||
DevelopUtils.inflateMenu(this, menu);
|
||||
}
|
||||
// 应用其他菜单
|
||||
getMenuInflater().inflate(R.menu.toolbar_main, menu);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int menuItemId = item.getItemId();
|
||||
if (AESThemeUtil.onAppThemeItemSelected(this, item)) {
|
||||
recreate();
|
||||
} if (DevelopUtils.onDevelopItemSelected(this, item)) {
|
||||
LogUtils.d(TAG, String.format("onOptionsItemSelected item.getItemId() %d ", item.getItemId()));
|
||||
} else if (menuItemId == R.id.item_settings) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
} else if (menuItemId == R.id.item_about) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(this, AboutActivity.class);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 绑定服务(仅用于获取服务状态,不启动服务)
|
||||
*/
|
||||
// private void bindDistanceService() {
|
||||
// Intent serviceIntent = new Intent(this, MainService.class);
|
||||
// // BIND_AUTO_CREATE:服务未启动则创建(仅为获取状态,启停由开关控制)
|
||||
// bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
|
||||
// }
|
||||
|
||||
// ---------------------- 核心功能3:页面跳转(位置管理页+日志页) ----------------------
|
||||
/**
|
||||
* 跳转至“位置管理页(LocationActivity)”(按钮点击触发,需在布局中设置 android:onClick="onPositions")
|
||||
* 服务未启动时提示,不允许跳转(避免LocationActivity无数据)
|
||||
*/
|
||||
public void onPositions(View view) {
|
||||
//ToastUtils.show("onPositions");
|
||||
// 服务已启动:跳转到位置管理页
|
||||
startActivity(new Intent(MainActivity.this, LocationActivity.class));
|
||||
}
|
||||
|
||||
// ---------------------- 新增:位置权限处理(适配Java7 + 后台GPS权限) ----------------------
|
||||
/**
|
||||
* 检查是否拥有「前台+后台」位置权限(适配Android版本差异)
|
||||
* Java7 特性:显式类型判断、无Lambda、兼容低版本API
|
||||
*/
|
||||
private boolean checkLocationPermissions() {
|
||||
// 1. 检查前台精确定位权限(Android 6.0+ 必需,显式强转权限常量)
|
||||
int foregroundPermResult = checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
boolean hasForegroundPerm = (foregroundPermResult == PackageManager.PERMISSION_GRANTED);
|
||||
|
||||
// 2. 检查后台定位权限(仅Android 10+ 需要,Java7 显式用Build.VERSION判断版本)
|
||||
boolean hasBackgroundPerm = true;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
int backgroundPermResult = checkSelfPermission(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION);
|
||||
hasBackgroundPerm = (backgroundPermResult == PackageManager.PERMISSION_GRANTED);
|
||||
}
|
||||
|
||||
// 前台+后台权限均满足,才返回true
|
||||
return hasForegroundPerm && hasBackgroundPerm;
|
||||
}
|
||||
|
||||
private void requestLocationPermissions() {
|
||||
// 1. 先判断前台定位权限(ACCESS_FINE_LOCATION)是否已授予
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
// 1.1 未授予前台权限:先申请前台权限(API 30+ 后台权限依赖前台权限)
|
||||
String[] foregroundPermissions = new String[]{Manifest.permission.ACCESS_FINE_LOCATION};
|
||||
// 对API 23+(Android 6.0)动态申请,低版本会直接授予(清单已声明前提下)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
requestPermissions(foregroundPermissions, REQUEST_LOCATION_PERMISSIONS);
|
||||
}
|
||||
} else {
|
||||
// 2. 已授予前台权限:判断是否需要申请后台权限(仅API 29+需要)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// 2.1 检查后台权限是否未授予
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_BACKGROUND_LOCATION)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
// 2.2 API 30+ 必须单独申请后台权限(不能和前台权限一起弹框)
|
||||
requestPermissions(
|
||||
new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
|
||||
REQUEST_BACKGROUND_LOCATION_PERMISSION
|
||||
);
|
||||
}
|
||||
}
|
||||
// 3. 前台权限已授予(+ 后台权限按需授予):此处可执行定位相关逻辑
|
||||
// doLocationRelatedLogic();
|
||||
}
|
||||
}
|
||||
|
||||
// 【必须补充】权限申请结果回调(处理用户同意/拒绝逻辑)
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
// 处理前台权限申请结果
|
||||
if (requestCode == REQUEST_LOCATION_PERMISSIONS) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// 前台权限同意:自动尝试申请后台权限(如果是API 29+)
|
||||
requestLocationPermissions();
|
||||
} else {
|
||||
// 前台权限拒绝:提示用户(可选:引导跳转到应用设置页)
|
||||
Toast.makeText(this, "需要前台定位权限才能使用该功能", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else if (requestCode == REQUEST_BACKGROUND_LOCATION_PERMISSION) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// 后台权限同意:可执行后台定位逻辑
|
||||
Toast.makeText(this, "已获得后台定位权限", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
// 后台权限拒绝:提示用户(可选:说明后台定位的用途,引导手动开启)
|
||||
Toast.makeText(this, "拒绝后台权限将无法在后台持续定位", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package cc.winboll.studio.positions.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
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;
|
||||
import cc.winboll.studio.positions.R;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/13 11:25
|
||||
* @Describe 应用介绍窗口
|
||||
*/
|
||||
public class AboutActivity extends WinBoLLActivity {
|
||||
|
||||
public static final String TAG = "AboutActivity";
|
||||
private Toolbar mToolbar;
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@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());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "导航栏 点击返回按钮");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
LogUtils.d(TAG, "initToolbar() 配置完成");
|
||||
}
|
||||
|
||||
private APPInfo genDefaultAppInfo() {
|
||||
LogUtils.d(TAG, "genDefaultAppInfo() 调用");
|
||||
String branchName = "positions";
|
||||
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("Positions");
|
||||
appInfo.setAppGitOwner("Studio");
|
||||
appInfo.setAppGitAPPBranch(branchName);
|
||||
appInfo.setAppGitAPPSubProjectFolder(branchName);
|
||||
appInfo.setAppHomePage("https://www.winboll.cc/apks/index.php?project=Positions");
|
||||
appInfo.setAppAPKName("Positions");
|
||||
appInfo.setAppAPKFolderName("Positions");
|
||||
LogUtils.d(TAG, "genDefaultAppInfo: 应用信息已生成");
|
||||
return appInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
package cc.winboll.studio.positions.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/29 18:22
|
||||
* @Describe 位置列表页面(适配MainService GPS接口+规范服务交互+完善生命周期)
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.positions.MainActivity;
|
||||
import cc.winboll.studio.positions.R;
|
||||
import cc.winboll.studio.positions.adapters.PositionAdapter;
|
||||
import cc.winboll.studio.positions.models.PositionModel;
|
||||
import cc.winboll.studio.positions.services.MainService;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Java 7 语法适配:
|
||||
* 1. 服务绑定用匿名内部类实现 ServiceConnection
|
||||
* 2. Adapter 初始化传入 MainService 实例,确保数据来源唯一
|
||||
* 3. 所有位置/任务操作通过 MainService 接口执行
|
||||
*/
|
||||
public class LocationActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
public static final String TAG = "LocationActivity";
|
||||
|
||||
private Toolbar mToolbar;
|
||||
|
||||
private RecyclerView mRvPosition;
|
||||
private PositionAdapter mPositionAdapter;
|
||||
|
||||
// MainService 引用+绑定状态(AtomicBoolean 确保多线程状态可见性)
|
||||
private MainService mMainService;
|
||||
private final AtomicBoolean isServiceBound = new AtomicBoolean(false);
|
||||
// 标记 Adapter 是否已初始化(避免重复初始化/销毁后初始化)
|
||||
private final AtomicBoolean isAdapterInited = new AtomicBoolean(false);
|
||||
|
||||
// ---------------------- 新增:GPS监听核心变量 ----------------------
|
||||
private MainService.GpsUpdateListener mGpsUpdateListener; // GPS监听实例
|
||||
private PositionModel mCurrentGpsPos; // 缓存当前GPS位置(供页面使用)
|
||||
// 本地位置缓存(解决服务数据未同步时Adapter空数据问题)
|
||||
private final ArrayList<PositionModel> mLocalPosCache = new ArrayList<PositionModel>();
|
||||
|
||||
|
||||
// 服务连接(Java 7 匿名内部类实现,强化状态同步+数据预加载)
|
||||
private ServiceConnection mServiceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
// 1. 安全获取服务实例(避免强转失败+服务未就绪)
|
||||
if (!(service instanceof MainService.LocalBinder)) {
|
||||
LogUtils.e(TAG, "服务绑定失败:Binder类型不匹配(非MainService.LocalBinder)");
|
||||
isServiceBound.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
MainService.LocalBinder binder = (MainService.LocalBinder) service;
|
||||
mMainService = binder.getService();
|
||||
// 2. 标记服务绑定成功(原子操作,确保多线程可见)
|
||||
isServiceBound.set(true);
|
||||
LogUtils.d(TAG, "MainService绑定成功,开始同步数据+初始化Adapter");
|
||||
|
||||
// 3. 同步服务数据到本地缓存(核心:先同步数据,再初始化Adapter)
|
||||
syncDataFromMainService();
|
||||
// 4. 注册GPS监听(确保监听在Adapter前初始化,数据不丢失)
|
||||
registerGpsListener();
|
||||
// 5. 初始化Adapter(传入本地缓存+服务实例,数据非空)
|
||||
initPositionAdapter();
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "服务绑定后初始化失败:" + e.getMessage());
|
||||
isServiceBound.set(false);
|
||||
mMainService = null;
|
||||
showToast("服务初始化失败,无法加载数据");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
LogUtils.w(TAG, "MainService断开连接,清空引用+标记状态");
|
||||
// 1. 清空服务引用+标记绑定状态
|
||||
mMainService = null;
|
||||
isServiceBound.set(false);
|
||||
// 2. 标记Adapter未初始化(下次绑定需重新初始化)
|
||||
isAdapterInited.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_location);
|
||||
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【导航栏】点击返回");
|
||||
startActivity(new Intent(LocationActivity.this, MainActivity.class));
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
// 1. 初始化视图(优先执行,避免Adapter初始化时视图为空)
|
||||
initView();
|
||||
// 2. 初始化GPS监听(提前创建,避免绑定服务后空指针)
|
||||
initGpsUpdateListener();
|
||||
// 3. 绑定MainService(最后执行,确保视图/监听已就绪)
|
||||
bindMainService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化视图(RecyclerView)- 确保视图先于Adapter初始化
|
||||
*/
|
||||
private void initView() {
|
||||
mRvPosition = (RecyclerView) findViewById(R.id.rv_position_list);
|
||||
// 1. 显式设置布局管理器(避免Adapter设置时无布局管理器崩溃)
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
|
||||
mRvPosition.setLayoutManager(layoutManager);
|
||||
// 2. 初始化本地缓存(避免首次加载时缓存为空)
|
||||
mLocalPosCache.clear();
|
||||
LogUtils.d(TAG, "视图初始化完成(布局管理器+本地缓存已就绪)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定MainService(Java 7 显式Intent,强化绑定安全性)
|
||||
*/
|
||||
private void bindMainService() {
|
||||
// 1. 避免重复绑定(快速重建Activity时防止多绑定)
|
||||
if (isServiceBound.get()) {
|
||||
LogUtils.w(TAG, "无需重复绑定:MainService已绑定");
|
||||
return;
|
||||
}
|
||||
|
||||
Intent serviceIntent = new Intent(this, MainService.class);
|
||||
// 2. 绑定服务(BIND_AUTO_CREATE:服务不存在时自动创建,增加绑定成功率)
|
||||
boolean bindSuccess = bindService(serviceIntent, mServiceConnection, BIND_AUTO_CREATE);
|
||||
if (!bindSuccess) {
|
||||
LogUtils.e(TAG, "发起MainService绑定请求失败(服务未找到/系统限制)");
|
||||
showToast("服务绑定失败,无法加载位置数据");
|
||||
} else {
|
||||
LogUtils.d(TAG, "MainService绑定请求已发起");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从MainService同步数据到本地缓存(核心解决Adapter空数据问题)
|
||||
* 作用:1. 服务数据优先同步到本地,Adapter基于本地缓存初始化
|
||||
* 2. 避免服务数据更新时直接操作Adapter,通过缓存中转
|
||||
*/
|
||||
private void syncDataFromMainService() {
|
||||
// 1. 安全校验(服务未绑定/服务空,用本地缓存兜底)
|
||||
if (!isServiceBound.get() || mMainService == null) {
|
||||
LogUtils.w(TAG, "同步数据:服务未就绪,使用本地缓存(当前缓存量=" + mLocalPosCache.size() + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 从服务获取最新位置数据(同步操作,确保数据拿到后再返回)
|
||||
ArrayList<PositionModel> servicePosList = mMainService.getPositionList();
|
||||
// 3. 同步到本地缓存(清空旧数据+添加新数据,避免重复)
|
||||
synchronized (mLocalPosCache) { // 加锁避免多线程操作缓存冲突
|
||||
mLocalPosCache.clear();
|
||||
if (servicePosList != null && !servicePosList.isEmpty()) {
|
||||
mLocalPosCache.addAll(servicePosList);
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "数据同步完成:服务位置数=" + (servicePosList == null ? 0 : servicePosList.size())
|
||||
+ ",本地缓存数=" + mLocalPosCache.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "同步服务数据失败:" + e.getMessage());
|
||||
// 异常时保留本地缓存,避免Adapter无数据
|
||||
LogUtils.w(TAG, "同步失败,使用本地缓存兜底(缓存量=" + mLocalPosCache.size() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化PositionAdapter(核心优化:基于本地缓存初始化,避免空数据)
|
||||
*/
|
||||
private void initPositionAdapter() {
|
||||
// 1. 多重安全校验(避免销毁后初始化/重复初始化/依赖未就绪)
|
||||
if (isAdapterInited.get() || !isServiceBound.get() || mMainService == null || mRvPosition == null) {
|
||||
LogUtils.w(TAG, "Adapter初始化跳过:"
|
||||
+ "已初始化=" + isAdapterInited.get()
|
||||
+ ",服务绑定=" + isServiceBound.get()
|
||||
+ ",视图就绪=" + (mRvPosition != null));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 基于本地缓存初始化Adapter(缓存已同步服务数据,非空)
|
||||
mPositionAdapter = new PositionAdapter(this, mLocalPosCache, mMainService);
|
||||
|
||||
// 3. 设置删除回调(删除时同步服务+本地缓存+Adapter)
|
||||
mPositionAdapter.setOnDeleteClickListener(new PositionAdapter.OnDeleteClickListener() {
|
||||
@Override
|
||||
public void onDeleteClick(final int position) {
|
||||
YesNoAlertDialog.show(LocationActivity.this, "删除位置提示", "是否删除此项锚点位置?", new YesNoAlertDialog.OnDialogResultListener(){
|
||||
|
||||
@Override
|
||||
public void onNo() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onYes() {
|
||||
// 安全校验(索引有效+服务绑定+缓存非空)
|
||||
if (position < 0 || position >= mLocalPosCache.size() || !isServiceBound.get() || mMainService == null) {
|
||||
LogUtils.w(TAG, "删除位置失败:索引无效/服务未就绪(索引=" + position + ",缓存量=" + mLocalPosCache.size() + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
PositionModel deletePos = mLocalPosCache.get(position);
|
||||
if (deletePos != null && !deletePos.getPositionId().isEmpty()) {
|
||||
// 步骤1:调用服务删除(确保服务数据一致性)
|
||||
mMainService.removePosition(deletePos.getPositionId());
|
||||
// 步骤2:删除本地缓存(确保缓存与服务同步)
|
||||
synchronized (mLocalPosCache) {
|
||||
mLocalPosCache.remove(position);
|
||||
}
|
||||
// 步骤3:通知Adapter刷新(基于缓存操作,避免空数据)
|
||||
mPositionAdapter.notifyItemRemoved(position);
|
||||
showToast("删除位置成功:" + deletePos.getMemo());
|
||||
LogUtils.d(TAG, "删除位置完成:ID=" + deletePos.getPositionId() + "(服务+缓存已同步)");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 设置保存回调(保存时同步服务+本地缓存+Adapter)
|
||||
mPositionAdapter.setOnSavePositionClickListener(new PositionAdapter.OnSavePositionClickListener() {
|
||||
@Override
|
||||
public void onSavePositionClick(int position, PositionModel updatedPos) {
|
||||
// 安全校验(索引有效+服务绑定+数据非空)
|
||||
if (!isServiceBound.get() || mMainService == null
|
||||
|| position < 0 || position >= mLocalPosCache.size() || updatedPos == null) {
|
||||
LogUtils.w(TAG, "保存位置失败:服务未就绪/索引无效/数据空");
|
||||
showToast("服务未就绪,保存失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 步骤1:调用服务更新(确保服务数据一致性)
|
||||
mMainService.updatePosition(updatedPos);
|
||||
// 步骤2:更新本地缓存(确保缓存与服务同步)
|
||||
synchronized (mLocalPosCache) {
|
||||
mLocalPosCache.set(position, updatedPos);
|
||||
}
|
||||
// 步骤3:通知Adapter刷新(基于缓存操作,避免空数据)
|
||||
mPositionAdapter.notifyItemChanged(position);
|
||||
showToast("保存位置成功:" + updatedPos.getMemo());
|
||||
LogUtils.d(TAG, "保存位置完成:ID=" + updatedPos.getPositionId() + "(服务+缓存已同步)");
|
||||
}
|
||||
});
|
||||
|
||||
// 5. 设置Adapter到RecyclerView(最后一步,确保Adapter已配置完成)
|
||||
mRvPosition.setAdapter(mPositionAdapter);
|
||||
// 6. 标记Adapter已初始化(避免重复初始化)
|
||||
isAdapterInited.set(true);
|
||||
LogUtils.d(TAG, "PositionAdapter初始化完成(基于本地缓存,数据量=" + mLocalPosCache.size() + ")");
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "Adapter初始化失败:" + e.getMessage());
|
||||
isAdapterInited.set(false);
|
||||
mPositionAdapter = null;
|
||||
showToast("位置列表初始化失败,请重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示Toast(Java 7 显式Toast.makeText,避免空Context)
|
||||
*/
|
||||
private void showToast(String content) {
|
||||
if (isFinishing() || isDestroyed()) { // 避免Activity销毁后弹Toast崩溃
|
||||
LogUtils.w(TAG, "Activity已销毁,跳过Toast:" + content);
|
||||
return;
|
||||
}
|
||||
Toast.makeText(this, content, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
// ---------------------- 页面交互(新增位置逻辑保留,适配GPS数据) ----------------------
|
||||
/**
|
||||
* 新增位置(调用服务addPosition(),可选:用当前GPS位置初始化新位置)
|
||||
*/
|
||||
public void addNewPosition(View view) {
|
||||
// 1. 隐藏软键盘(避免软键盘遮挡操作)
|
||||
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null && getCurrentFocus() != null) {
|
||||
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
|
||||
}
|
||||
|
||||
// 2. 安全校验(服务未绑定,不允许新增)
|
||||
if (!isServiceBound.get() || mMainService == null) {
|
||||
LogUtils.w(TAG, "新增位置失败:MainService未绑定");
|
||||
showToast("服务未就绪,无法新增位置");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 创建新位置模型(优化:优先用当前GPS位置初始化,无则用默认值)
|
||||
PositionModel newPos = new PositionModel();
|
||||
newPos.setPositionId(PositionModel.genPositionId()); // 生成唯一ID(需PositionModel实现)
|
||||
if (mCurrentGpsPos != null) {
|
||||
newPos.setLongitude(mCurrentGpsPos.getLongitude());
|
||||
newPos.setLatitude(mCurrentGpsPos.getLatitude());
|
||||
newPos.setMemo("当前GPS位置(可编辑)");
|
||||
} else {
|
||||
newPos.setLongitude(116.404267); // 北京经度(默认值)
|
||||
newPos.setLatitude(39.915119); // 北京纬度(默认值)
|
||||
newPos.setMemo("默认位置(可编辑备注)");
|
||||
}
|
||||
newPos.setIsSimpleView(true); // 默认简单视图
|
||||
newPos.setIsEnableRealPositionDistance(true); // 启用距离计算(依赖GPS)
|
||||
|
||||
// 4. 调用服务新增+同步本地缓存(确保缓存与服务一致)
|
||||
mMainService.addPosition(newPos);
|
||||
synchronized (mLocalPosCache) {
|
||||
mLocalPosCache.add(newPos);
|
||||
}
|
||||
LogUtils.d(TAG, "通过服务新增位置:ID=" + newPos.getPositionId() + ",纬度=" + newPos.getLatitude() + "(缓存已同步)");
|
||||
|
||||
// 5. 刷新Adapter(基于缓存操作,确保数据立即显示)
|
||||
if (isAdapterInited.get() && mPositionAdapter != null) {
|
||||
mPositionAdapter.notifyItemInserted(mLocalPosCache.size() - 1);
|
||||
}
|
||||
showToast("新增位置成功(已启用GPS距离计算)");
|
||||
}
|
||||
|
||||
// ---------------------- 新增:GPS监听初始化+注册/反注册(核心适配逻辑) ----------------------
|
||||
/**
|
||||
* 初始化GPS监听:实现MainService.GpsUpdateListener,接收实时GPS数据
|
||||
*/
|
||||
private void initGpsUpdateListener() {
|
||||
LogUtils.d(TAG, "initGpsUpdateListener()");
|
||||
mGpsUpdateListener = new MainService.GpsUpdateListener() {
|
||||
@Override
|
||||
public void onGpsPositionUpdated(PositionModel currentGpsPos) {
|
||||
if (currentGpsPos == null || isFinishing() || isDestroyed()) {
|
||||
LogUtils.w(TAG, "GPS位置更新:数据为空或Activity已销毁");
|
||||
return;
|
||||
}
|
||||
// 缓存当前GPS位置(供页面其他逻辑使用)
|
||||
mCurrentGpsPos = currentGpsPos;
|
||||
LogUtils.d(TAG, String.format("收到GPS更新:纬度=%.4f,经度=%.4f"
|
||||
, currentGpsPos.getLatitude(), currentGpsPos.getLongitude()));
|
||||
// 安全更新UI(避免Activity销毁后操作视图崩溃)
|
||||
((TextView)findViewById(R.id.tv_latitude)).setText(String.format("当前纬度:%f", currentGpsPos.getLatitude()));
|
||||
((TextView)findViewById(R.id.tv_longitude)).setText(String.format("当前经度:%f", currentGpsPos.getLongitude()));
|
||||
// 设置格式化后的时间字符串
|
||||
long currentTime = System.currentTimeMillis();
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
|
||||
String timeStr = sdf.format(new Date(currentTime));
|
||||
// 设置当前时间
|
||||
((TextView)findViewById(R.id.tv_timenow)).setText("现在时间:" + timeStr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGpsStatusChanged(String status) {
|
||||
if (status == null || isFinishing() || isDestroyed()) return;
|
||||
LogUtils.d(TAG, "GPS状态变化:" + status);
|
||||
if (status.contains("未开启") || status.contains("权限") || status.contains("失败")) {
|
||||
ToastUtils.show("GPS提示:" + status);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册GPS监听:调用MainService的PUBLIC方法,绑定监听
|
||||
*/
|
||||
private void registerGpsListener() {
|
||||
// 安全校验(避免Activity销毁/服务未绑定/监听为空时注册)
|
||||
if (isFinishing() || isDestroyed() || !isServiceBound.get() || mMainService == null || mGpsUpdateListener == null) {
|
||||
LogUtils.w(TAG, "GPS监听注册跳过:Activity状态异常/依赖未就绪");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mMainService.registerGpsUpdateListener(mGpsUpdateListener);
|
||||
LogUtils.d(TAG, "GPS监听已注册");
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "GPS监听注册失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反注册GPS监听:调用MainService的PUBLIC方法,解绑监听(核心防内存泄漏+数据异常)
|
||||
*/
|
||||
private void unregisterGpsListener() {
|
||||
// 避免Activity销毁后调用服务方法(防止空指针/服务已解绑)
|
||||
if (mMainService == null || mGpsUpdateListener == null) {
|
||||
LogUtils.w(TAG, "GPS监听反注册跳过:服务/监听未初始化");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mMainService.unregisterGpsUpdateListener(mGpsUpdateListener);
|
||||
LogUtils.d(TAG, "GPS监听已反注册");
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "GPS监听反注册失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面可见时同步数据(解决快速切回时数据未更新问题)
|
||||
* 场景:快速关闭再打开Activity,服务已绑定但数据未重新同步
|
||||
*/
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
// 1. 服务已绑定但Adapter未初始化:重新同步数据+初始化Adapter
|
||||
if (isServiceBound.get() && mMainService != null && !isAdapterInited.get()) {
|
||||
LogUtils.d(TAG, "onResume:服务已绑定但Adapter未初始化,重新同步数据");
|
||||
syncDataFromMainService();
|
||||
initPositionAdapter();
|
||||
} else if (isServiceBound.get() && mMainService != null && isAdapterInited.get() && mPositionAdapter != null) {
|
||||
syncDataFromMainService();
|
||||
mPositionAdapter.notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "onResume:刷新位置数据(与服务同步)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面不可见时暂停操作(避免后台操作导致数据异常)
|
||||
*/
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
// 避免后台时仍执行UI刷新(如GPS更新触发的视图操作)
|
||||
LogUtils.d(TAG, "onPause:页面不可见,暂停UI相关操作");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LogUtils.d(TAG, "onDestroy:开始释放资源");
|
||||
|
||||
// 1. 反注册GPS监听(优先执行,避免服务持有Activity引用导致内存泄漏)
|
||||
unregisterGpsListener();
|
||||
|
||||
// 2. 释放Adapter资源(反注册可能的监听,避免内存泄漏)
|
||||
if (mPositionAdapter != null) {
|
||||
mPositionAdapter.release();
|
||||
mPositionAdapter = null; // 清空引用,帮助GC回收
|
||||
LogUtils.d(TAG, "Adapter资源已释放");
|
||||
}
|
||||
|
||||
// 3. 解绑MainService(最后执行,确保其他资源已释放)
|
||||
if (isServiceBound.get()) {
|
||||
try {
|
||||
unbindService(mServiceConnection);
|
||||
LogUtils.d(TAG, "MainService解绑完成");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 捕获“服务未绑定”异常(快速开关时可能出现,避免崩溃)
|
||||
LogUtils.d(TAG, "解绑MainService失败:服务未绑定(可能已提前解绑)");
|
||||
}
|
||||
// 重置绑定状态+服务引用
|
||||
isServiceBound.set(false);
|
||||
mMainService = null;
|
||||
}
|
||||
|
||||
// 4. 清空本地缓存+GPS引用(帮助GC回收)
|
||||
synchronized (mLocalPosCache) {
|
||||
mLocalPosCache.clear();
|
||||
}
|
||||
mCurrentGpsPos = null;
|
||||
mGpsUpdateListener = null;
|
||||
isAdapterInited.set(false);
|
||||
LogUtils.d(TAG, "所有资源释放完成(onDestroy执行结束)");
|
||||
}
|
||||
|
||||
// ---------------------- 移除重复定义:LocalBinder 统一在 MainService 中定义 ----------------------
|
||||
// 说明:原LocationActivity中的LocalBinder是重复定义(MainService已实现),会导致类型强转失败
|
||||
// 此处删除该类,确保Activity绑定服务时强转的是MainService中的LocalBinder
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package cc.winboll.studio.positions.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import cc.winboll.studio.libaes.interfaces.IWinBoLLActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.positions.R;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/12/07 23:29
|
||||
* @Describe 应用设置活动窗口
|
||||
*/
|
||||
public class SettingsActivity extends WinBoLLActivity implements IWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "SettingsActivity";
|
||||
|
||||
private Toolbar mToolbar;
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
|
||||
mToolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(mToolbar);
|
||||
mToolbar.setSubtitle(getTag());
|
||||
mToolbar.setTitleTextAppearance(this, R.style.Toolbar_TitleText);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "【导航栏】点击返回");
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package cc.winboll.studio.positions.activities;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/29 00:11
|
||||
* @Describe WinBoLL 窗口基础类
|
||||
*/
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
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;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
public class WinBoLLActivity extends AppCompatActivity implements IWinBoLLActivity {
|
||||
|
||||
public static final String TAG = "WinBoLLActivity";
|
||||
|
||||
protected volatile AESThemeBean.ThemeType mThemeType;
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
mThemeType = getThemeType();
|
||||
setThemeStyle();
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
AESThemeBean.ThemeType getThemeType() {
|
||||
return AESThemeBean.getThemeStyleType(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||
}
|
||||
|
||||
void setThemeStyle() {
|
||||
setTheme(AESThemeUtil.getThemeTypeID(getApplicationContext()));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
LogUtils.d(TAG, String.format("onResume %s", getTag()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
/*if (item.getItemId() == R.id.item_log) {
|
||||
WinBoLLActivityManager.getInstance().startLogActivity(this);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.item_home) {
|
||||
startActivity(new Intent(this, MainActivity.class));
|
||||
return true;
|
||||
}*/
|
||||
// 在switch语句中处理每个ID,并在处理完后返回true,未处理的情况返回false。
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
WinBoLLActivityManager.getInstance().add(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
WinBoLLActivityManager.getInstance().registeRemove(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,639 @@
|
||||
package cc.winboll.studio.positions.adapters;
|
||||
|
||||
/**
|
||||
* @Describe 位置数据适配器(修复视图复用资源加载,支持滚动后重新绑定数据,Java 7语法适配)
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @CreateTime 2025-09-29 20:25:00
|
||||
* @EditTime 2026-03-31 23:14:55
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.positions.R;
|
||||
import cc.winboll.studio.positions.models.PositionModel;
|
||||
import cc.winboll.studio.positions.models.PositionTaskModel;
|
||||
import cc.winboll.studio.positions.services.MainService;
|
||||
import cc.winboll.studio.positions.views.PositionTaskListView;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class PositionAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
implements MainService.TaskUpdateListener {
|
||||
|
||||
public static final String TAG = "PositionAdapter";
|
||||
|
||||
// 视图类型常量
|
||||
private static final int VIEW_TYPE_SIMPLE = 0;
|
||||
private static final int VIEW_TYPE_EDIT = 1;
|
||||
|
||||
// 默认配置常量
|
||||
private static final String DEFAULT_MEMO = "无备注";
|
||||
private static final String DEFAULT_TASK_DESC = "新任务";
|
||||
private static final int DEFAULT_TASK_DISTANCE = 50;
|
||||
private static final String DISTANCE_FORMAT = "实时距离:%.1f 米";
|
||||
private static final String DISTANCE_DISABLED = "实时距离:未启用";
|
||||
private static final String DISTANCE_ERROR = "实时距离:计算失败";
|
||||
|
||||
// 核心成员变量
|
||||
private final Context mContext;
|
||||
private final ArrayList<PositionModel> mCachedPositionList;
|
||||
private final WeakReference<MainService> mMainServiceRef;
|
||||
private final ConcurrentHashMap<String, PositionTaskListView> mSimpleTaskViewMap;
|
||||
private final ConcurrentHashMap<String, PositionTaskListView> mEditTaskViewMap;
|
||||
private final ConcurrentHashMap<String, TextView> mPosDistanceViewMap;
|
||||
|
||||
// 回调接口
|
||||
public interface OnDeleteClickListener {
|
||||
void onDeleteClick(int position);
|
||||
}
|
||||
|
||||
public interface OnSavePositionClickListener {
|
||||
void onSavePositionClick(int position, PositionModel updatedPos);
|
||||
}
|
||||
|
||||
private OnDeleteClickListener mOnDeleteListener;
|
||||
private OnSavePositionClickListener mOnSavePosListener;
|
||||
|
||||
// =========================================================================
|
||||
// 构造函数
|
||||
// =========================================================================
|
||||
public PositionAdapter(Context context, ArrayList<PositionModel> cachedPositionList, MainService mainService) {
|
||||
LogUtils.d(TAG, "PositionAdapter 构造函数开始,context=" + context
|
||||
+ ",cachedPositionList=" + (cachedPositionList != null ? cachedPositionList.size() : 0)
|
||||
+ ",mainService=" + mainService);
|
||||
|
||||
this.mContext = context;
|
||||
this.mCachedPositionList = (cachedPositionList != null) ? cachedPositionList : new ArrayList<PositionModel>();
|
||||
this.mMainServiceRef = new WeakReference<MainService>(mainService);
|
||||
this.mSimpleTaskViewMap = new ConcurrentHashMap<String, PositionTaskListView>();
|
||||
this.mEditTaskViewMap = new ConcurrentHashMap<String, PositionTaskListView>();
|
||||
this.mPosDistanceViewMap = new ConcurrentHashMap<String, TextView>();
|
||||
|
||||
if (mainService != null) {
|
||||
mainService.registerTaskUpdateListener(this);
|
||||
LogUtils.d(TAG, "已注册 MainService 任务监听");
|
||||
} else {
|
||||
LogUtils.w(TAG, "构造函数:MainService 为空,无法初始化任务视图");
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, "PositionAdapter 初始化完成,位置数量=" + mCachedPositionList.size());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// RecyclerView 核心方法
|
||||
// =========================================================================
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
PositionModel posModel = getPositionByIndex(position);
|
||||
return (posModel != null && posModel.isSimpleView()) ? VIEW_TYPE_SIMPLE : VIEW_TYPE_EDIT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LogUtils.d(TAG, "onCreateViewHolder viewType=" + viewType);
|
||||
LayoutInflater inflater = LayoutInflater.from(mContext);
|
||||
if (viewType == VIEW_TYPE_SIMPLE) {
|
||||
View simpleView = inflater.inflate(R.layout.item_position_simple, parent, false);
|
||||
return new SimpleViewHolder(simpleView);
|
||||
} else {
|
||||
View editView = inflater.inflate(R.layout.item_position_edit, parent, false);
|
||||
return new EditViewHolder(editView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
LogUtils.d(TAG, "onBindViewHolder position=" + position);
|
||||
PositionModel posModel = getPositionByIndex(position);
|
||||
if (posModel == null) {
|
||||
LogUtils.w(TAG, "onBindViewHolder:位置模型为空,索引=" + position);
|
||||
return;
|
||||
}
|
||||
final String posId = posModel.getPositionId();
|
||||
MainService mainService = mMainServiceRef.get();
|
||||
|
||||
if (holder instanceof SimpleViewHolder) {
|
||||
LogUtils.d(TAG, "绑定 SimpleViewHolder,posId=" + posId);
|
||||
SimpleViewHolder simpleHolder = (SimpleViewHolder) holder;
|
||||
bindSimplePositionData(simpleHolder, posModel);
|
||||
initAndBindSimpleTaskView(simpleHolder.ptlvSimpleTasks, posId, mainService);
|
||||
mSimpleTaskViewMap.put(posId, simpleHolder.ptlvSimpleTasks);
|
||||
mPosDistanceViewMap.put(posId, simpleHolder.tvSimpleDistance);
|
||||
|
||||
} else if (holder instanceof EditViewHolder) {
|
||||
LogUtils.d(TAG, "绑定 EditViewHolder,posId=" + posId);
|
||||
EditViewHolder editHolder = (EditViewHolder) holder;
|
||||
bindEditPositionData(editHolder, posModel, position);
|
||||
initAndBindEditTaskView(editHolder.ptlvEditTasks, posId, mainService, editHolder.btnAddTask);
|
||||
mEditTaskViewMap.put(posId, editHolder.ptlvEditTasks);
|
||||
mPosDistanceViewMap.put(posId, editHolder.tvEditDistance);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) {
|
||||
super.onViewDetachedFromWindow(holder);
|
||||
PositionModel posModel = getPositionByIndex(holder.getAdapterPosition());
|
||||
if (posModel == null || TextUtils.isEmpty(posModel.getPositionId())) {
|
||||
return;
|
||||
}
|
||||
String posId = posModel.getPositionId();
|
||||
|
||||
if (mPosDistanceViewMap.containsKey(posId)) {
|
||||
TextView distanceView = mPosDistanceViewMap.get(posId);
|
||||
if (distanceView == null || !distanceView.isAttachedToWindow()) {
|
||||
mPosDistanceViewMap.remove(posId);
|
||||
LogUtils.d(TAG, "视图脱离:移除无效距离控件缓存 posId=" + posId);
|
||||
}
|
||||
}
|
||||
|
||||
if (holder instanceof SimpleViewHolder && mSimpleTaskViewMap.containsKey(posId)) {
|
||||
PositionTaskListView taskView = mSimpleTaskViewMap.get(posId);
|
||||
if (taskView == null || !taskView.isAttachedToWindow()) {
|
||||
mSimpleTaskViewMap.remove(posId);
|
||||
LogUtils.d(TAG, "视图脱离:移除无效简单任务视图缓存 posId=" + posId);
|
||||
}
|
||||
}
|
||||
|
||||
if (holder instanceof EditViewHolder && mEditTaskViewMap.containsKey(posId)) {
|
||||
PositionTaskListView taskView = mEditTaskViewMap.get(posId);
|
||||
if (taskView == null || !taskView.isAttachedToWindow()) {
|
||||
mEditTaskViewMap.remove(posId);
|
||||
LogUtils.d(TAG, "视图脱离:移除无效编辑任务视图缓存 posId=" + posId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mCachedPositionList.size();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 位置数据绑定
|
||||
// =========================================================================
|
||||
private void bindSimplePositionData(SimpleViewHolder holder, final PositionModel posModel) {
|
||||
LogUtils.d(TAG, "bindSimplePositionData posId=" + posModel.getPositionId());
|
||||
holder.tvSimpleLon.setText(String.format("经度:%.6f", posModel.getLongitude()));
|
||||
holder.tvSimpleLat.setText(String.format("纬度:%.6f", posModel.getLatitude()));
|
||||
|
||||
String memo = posModel.getMemo();
|
||||
holder.tvSimpleMemo.setText("备注:" + (TextUtils.isEmpty(memo) ? DEFAULT_MEMO : memo));
|
||||
updateDistanceDisplay(holder.tvSimpleDistance, posModel);
|
||||
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
posModel.setIsSimpleView(false);
|
||||
notifyItemChanged(getPositionIndexById(posModel.getPositionId()));
|
||||
LogUtils.d(TAG, "简单视图点击:切换编辑模式 posId=" + posModel.getPositionId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void bindEditPositionData(final EditViewHolder holder, final PositionModel posModel, final int position) {
|
||||
LogUtils.d(TAG, "bindEditPositionData posId=" + posModel.getPositionId() + " position=" + position);
|
||||
final String posId = posModel.getPositionId();
|
||||
|
||||
holder.tvEditLon.setText(String.format("经度:%.6f", posModel.getLongitude()));
|
||||
holder.tvEditLat.setText(String.format("纬度:%.6f", posModel.getLatitude()));
|
||||
|
||||
String memo = posModel.getMemo();
|
||||
if (!TextUtils.isEmpty(memo)) {
|
||||
holder.etEditMemo.setText(memo);
|
||||
holder.etEditMemo.setSelection(memo.length());
|
||||
} else {
|
||||
holder.etEditMemo.setText("");
|
||||
}
|
||||
|
||||
updateDistanceDisplay(holder.tvEditDistance, posModel);
|
||||
|
||||
holder.rgDistanceSwitch.check(posModel.isEnableRealPositionDistance()
|
||||
? R.id.rb_distance_enable
|
||||
: R.id.rb_distance_disable);
|
||||
|
||||
holder.btnCancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
posModel.setIsSimpleView(true);
|
||||
notifyItemChanged(position);
|
||||
hideSoftKeyboard(v);
|
||||
LogUtils.d(TAG, "取消编辑:切换简单视图 posId=" + posId);
|
||||
}
|
||||
});
|
||||
|
||||
holder.btnDelete.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mOnDeleteListener != null) {
|
||||
mOnDeleteListener.onDeleteClick(position);
|
||||
}
|
||||
hideSoftKeyboard(v);
|
||||
LogUtils.d(TAG, "触发删除位置 posId=" + posId);
|
||||
}
|
||||
});
|
||||
|
||||
holder.btnSave.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String newMemo = holder.etEditMemo.getText().toString().trim();
|
||||
boolean isDistanceEnable = (holder.rgDistanceSwitch.getCheckedRadioButtonId() == R.id.rb_distance_enable);
|
||||
|
||||
PositionModel updatedPos = new PositionModel();
|
||||
updatedPos.setPositionId(posId);
|
||||
updatedPos.setLongitude(posModel.getLongitude());
|
||||
updatedPos.setLatitude(posModel.getLatitude());
|
||||
updatedPos.setMemo(newMemo);
|
||||
updatedPos.setIsEnableRealPositionDistance(isDistanceEnable);
|
||||
updatedPos.setIsSimpleView(true);
|
||||
|
||||
if (mOnSavePosListener != null) {
|
||||
mOnSavePosListener.onSavePositionClick(position, updatedPos);
|
||||
}
|
||||
|
||||
posModel.setMemo(newMemo);
|
||||
posModel.setIsEnableRealPositionDistance(isDistanceEnable);
|
||||
posModel.setIsSimpleView(true);
|
||||
notifyItemChanged(position);
|
||||
hideSoftKeyboard(v);
|
||||
|
||||
LogUtils.d(TAG, "保存位置 posId=" + posId + " 新备注=" + newMemo + " 距离启用=" + isDistanceEnable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PositionTaskListView 集成
|
||||
// =========================================================================
|
||||
private void initAndBindSimpleTaskView(PositionTaskListView taskView, String posId, MainService mainService) {
|
||||
LogUtils.d(TAG, "initAndBindSimpleTaskView posId=" + posId);
|
||||
if (taskView == null || TextUtils.isEmpty(posId) || mainService == null) {
|
||||
LogUtils.w(TAG, "初始化简单任务视图失败:参数无效");
|
||||
return;
|
||||
}
|
||||
taskView.init(mainService, posId);
|
||||
taskView.setViewStatus(PositionTaskListView.VIEW_MODE_SIMPLE);
|
||||
taskView.syncTasksFromMainService();
|
||||
|
||||
taskView.setOnTaskUpdatedListener(new PositionTaskListView.OnTaskUpdatedListener() {
|
||||
@Override
|
||||
public void onTaskUpdated(String positionId, ArrayList updatedTasks) {
|
||||
LogUtils.d(TAG, "简单模式任务更新 posId=" + positionId + " 任务数=" + updatedTasks.size());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initAndBindEditTaskView(final PositionTaskListView taskView, final String posId,
|
||||
MainService mainService, Button btnAddTask) {
|
||||
LogUtils.d(TAG, "initAndBindEditTaskView posId=" + posId);
|
||||
if (taskView == null || TextUtils.isEmpty(posId) || mainService == null || btnAddTask == null) {
|
||||
LogUtils.w(TAG, "初始化编辑任务视图失败:参数无效");
|
||||
return;
|
||||
}
|
||||
taskView.init(mainService, posId);
|
||||
taskView.setViewStatus(PositionTaskListView.VIEW_MODE_EDIT);
|
||||
taskView.syncTasksFromMainService();
|
||||
|
||||
btnAddTask.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
PositionTaskModel newTask = new PositionTaskModel();
|
||||
newTask.setTaskId(PositionTaskModel.genTaskId());
|
||||
newTask.setPositionId(posId);
|
||||
newTask.setTaskDescription(DEFAULT_TASK_DESC);
|
||||
newTask.setDiscussDistance(DEFAULT_TASK_DISTANCE);
|
||||
newTask.setIsEnable(true);
|
||||
newTask.setIsBingo(false);
|
||||
|
||||
taskView.addNewTask(newTask);
|
||||
hideSoftKeyboard(v);
|
||||
LogUtils.d(TAG, "新增任务 posId=" + posId + " taskId=" + newTask.getTaskId());
|
||||
}
|
||||
});
|
||||
|
||||
taskView.setOnTaskUpdatedListener(new PositionTaskListView.OnTaskUpdatedListener() {
|
||||
@Override
|
||||
public void onTaskUpdated(String positionId, ArrayList updatedTasks) {
|
||||
LogUtils.d(TAG, "编辑模式任务更新 posId=" + positionId + " 任务数=" + updatedTasks.size());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 工具方法
|
||||
// =========================================================================
|
||||
private void updateDistanceDisplay(TextView distanceView, PositionModel posModel) {
|
||||
if (distanceView == null || posModel == null) {
|
||||
LogUtils.w(TAG, "updateDistanceDisplay:参数为空");
|
||||
return;
|
||||
}
|
||||
if (!posModel.isEnableRealPositionDistance()) {
|
||||
distanceView.setText(DISTANCE_DISABLED);
|
||||
distanceView.setTextColor(mContext.getResources().getColor(R.color.gray));
|
||||
return;
|
||||
}
|
||||
double distance = posModel.getRealPositionDistance();
|
||||
if (distance < 0) {
|
||||
distanceView.setText(DISTANCE_ERROR);
|
||||
distanceView.setTextColor(mContext.getResources().getColor(R.color.red));
|
||||
return;
|
||||
}
|
||||
distanceView.setText(String.format(DISTANCE_FORMAT, distance));
|
||||
if (distance <= 100) {
|
||||
distanceView.setTextColor(mContext.getResources().getColor(R.color.green));
|
||||
} else if (distance <= 500) {
|
||||
distanceView.setTextColor(mContext.getResources().getColor(R.color.yellow));
|
||||
} else {
|
||||
distanceView.setTextColor(mContext.getResources().getColor(R.color.red));
|
||||
}
|
||||
}
|
||||
|
||||
private PositionModel getPositionByIndex(int index) {
|
||||
if (mCachedPositionList == null || index < 0 || index >= mCachedPositionList.size()) {
|
||||
LogUtils.w(TAG, "getPositionByIndex:无效索引 index=" + index);
|
||||
return null;
|
||||
}
|
||||
return mCachedPositionList.get(index);
|
||||
}
|
||||
|
||||
private int getPositionIndexById(String positionId) {
|
||||
if (TextUtils.isEmpty(positionId) || mCachedPositionList == null || mCachedPositionList.isEmpty()) {
|
||||
LogUtils.w(TAG, "getPositionIndexById:参数无效");
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < mCachedPositionList.size(); i++) {
|
||||
PositionModel pos = mCachedPositionList.get(i);
|
||||
if (positionId.equals(pos.getPositionId())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
LogUtils.w(TAG, "未找到位置ID=" + positionId);
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void updateSinglePositionDistance(String positionId) {
|
||||
LogUtils.d(TAG, "updateSinglePositionDistance posId=" + positionId);
|
||||
if (TextUtils.isEmpty(positionId) || mPosDistanceViewMap.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
MainService mainService = getMainServiceWithRetry(2);
|
||||
if (mainService == null) {
|
||||
LogUtils.e(TAG, "无法获取 MainService");
|
||||
return;
|
||||
}
|
||||
PositionModel latestPos = null;
|
||||
try {
|
||||
ArrayList servicePosList = mainService.getPositionList();
|
||||
if (servicePosList != null && !servicePosList.isEmpty()) {
|
||||
Iterator iter = servicePosList.iterator();
|
||||
while (iter.hasNext()) {
|
||||
PositionModel pos = (PositionModel) iter.next();
|
||||
if (positionId.equals(pos.getPositionId())) {
|
||||
latestPos = pos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "获取位置数据异常", e);
|
||||
return;
|
||||
}
|
||||
if (latestPos == null) {
|
||||
LogUtils.w(TAG, "未找到位置 posId=" + positionId);
|
||||
return;
|
||||
}
|
||||
final TextView distanceView = mPosDistanceViewMap.get(positionId);
|
||||
if (distanceView != null && distanceView.isAttachedToWindow()) {
|
||||
final PositionModel finalLatestPos = latestPos;
|
||||
distanceView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateDistanceDisplay(distanceView, finalLatestPos);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mPosDistanceViewMap.remove(positionId);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateAllPositionData(ArrayList<PositionModel> newPosList) {
|
||||
LogUtils.d(TAG, "updateAllPositionData 新数据数量=" + (newPosList != null ? newPosList.size() : 0));
|
||||
if (newPosList == null) {
|
||||
return;
|
||||
}
|
||||
ArrayList<PositionModel> validPosList = new ArrayList<PositionModel>();
|
||||
for (PositionModel pos : newPosList) {
|
||||
if (TextUtils.isEmpty(pos.getPositionId())
|
||||
|| pos.getLongitude() < -180 || pos.getLongitude() > 180
|
||||
|| pos.getLatitude() < -90 || pos.getLatitude() > 90) {
|
||||
continue;
|
||||
}
|
||||
validPosList.add(pos);
|
||||
}
|
||||
ConcurrentHashMap<String, PositionModel> uniquePosMap = new ConcurrentHashMap<String, PositionModel>();
|
||||
for (PositionModel pos : validPosList) {
|
||||
uniquePosMap.put(pos.getPositionId(), pos);
|
||||
}
|
||||
ArrayList uniquePosList = new ArrayList(uniquePosMap.values());
|
||||
|
||||
mCachedPositionList.clear();
|
||||
mCachedPositionList.addAll(uniquePosList);
|
||||
mPosDistanceViewMap.clear();
|
||||
mSimpleTaskViewMap.clear();
|
||||
mEditTaskViewMap.clear();
|
||||
notifyDataSetChanged();
|
||||
|
||||
LogUtils.d(TAG, "全量更新完成,有效数量=" + uniquePosList.size());
|
||||
}
|
||||
|
||||
private void hideSoftKeyboard(View view) {
|
||||
if (mContext == null || view == null) {
|
||||
return;
|
||||
}
|
||||
InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null) {
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
private MainService getMainServiceWithRetry(int retryCount) {
|
||||
MainService mainService = mMainServiceRef.get();
|
||||
if (mainService != null) {
|
||||
return mainService;
|
||||
}
|
||||
for (int i = 0; i < retryCount; i++) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
mainService = mMainServiceRef.get();
|
||||
if (mainService != null) {
|
||||
LogUtils.d(TAG, "重试获取 MainService 成功,第" + (i + 1) + "次");
|
||||
return mainService;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, "重试被中断", e);
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
LogUtils.e(TAG, "重试" + retryCount + "次仍未获取到 MainService");
|
||||
return null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 任务更新监听
|
||||
// =========================================================================
|
||||
@Override
|
||||
public void onTaskUpdated() {
|
||||
LogUtils.d(TAG, "onTaskUpdated:收到服务任务更新");
|
||||
if (!mSimpleTaskViewMap.isEmpty()) {
|
||||
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mSimpleTaskViewMap.entrySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
ConcurrentHashMap.Entry<String, PositionTaskListView> entry = iter.next();
|
||||
PositionTaskListView taskView = entry.getValue();
|
||||
if (taskView != null && taskView.isAttachedToWindow()) {
|
||||
taskView.syncTasksFromMainService();
|
||||
} else {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!mEditTaskViewMap.isEmpty()) {
|
||||
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mEditTaskViewMap.entrySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
ConcurrentHashMap.Entry<String, PositionTaskListView> entry = iter.next();
|
||||
PositionTaskListView taskView = entry.getValue();
|
||||
if (taskView != null && taskView.isAttachedToWindow()) {
|
||||
taskView.syncTasksFromMainService();
|
||||
} else {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 外部回调设置
|
||||
// =========================================================================
|
||||
public void setOnDeleteClickListener(OnDeleteClickListener listener) {
|
||||
LogUtils.d(TAG, "setOnDeleteClickListener listener=" + listener);
|
||||
this.mOnDeleteListener = listener;
|
||||
}
|
||||
|
||||
public void setOnSavePositionClickListener(OnSavePositionClickListener listener) {
|
||||
LogUtils.d(TAG, "setOnSavePositionClickListener listener=" + listener);
|
||||
this.mOnSavePosListener = listener;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 资源释放
|
||||
// =========================================================================
|
||||
public void release() {
|
||||
LogUtils.d(TAG, "release:开始释放 Adapter 资源");
|
||||
MainService mainService = mMainServiceRef.get();
|
||||
if (mainService != null) {
|
||||
mainService.unregisterTaskUpdateListener(this);
|
||||
LogUtils.d(TAG, "已反注册任务监听");
|
||||
}
|
||||
|
||||
if (!mSimpleTaskViewMap.isEmpty()) {
|
||||
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mSimpleTaskViewMap.entrySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
PositionTaskListView taskView = iter.next().getValue();
|
||||
if (taskView != null) {
|
||||
taskView.clearData();
|
||||
taskView.setOnTaskUpdatedListener(null);
|
||||
}
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
if (!mEditTaskViewMap.isEmpty()) {
|
||||
Iterator<ConcurrentHashMap.Entry<String, PositionTaskListView>> iter = mEditTaskViewMap.entrySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
PositionTaskListView taskView = iter.next().getValue();
|
||||
if (taskView != null) {
|
||||
taskView.clearData();
|
||||
taskView.setOnTaskUpdatedListener(null);
|
||||
}
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
mPosDistanceViewMap.clear();
|
||||
if (mCachedPositionList != null) {
|
||||
mCachedPositionList.clear();
|
||||
}
|
||||
mOnDeleteListener = null;
|
||||
mOnSavePosListener = null;
|
||||
|
||||
LogUtils.d(TAG, "release:资源释放完成");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ViewHolder
|
||||
// =========================================================================
|
||||
public static class SimpleViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvSimpleLon;
|
||||
TextView tvSimpleLat;
|
||||
TextView tvSimpleMemo;
|
||||
TextView tvSimpleDistance;
|
||||
PositionTaskListView ptlvSimpleTasks;
|
||||
|
||||
public SimpleViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
tvSimpleLon = (TextView) itemView.findViewById(R.id.tv_simple_longitude);
|
||||
tvSimpleLat = (TextView) itemView.findViewById(R.id.tv_simple_latitude);
|
||||
tvSimpleMemo = (TextView) itemView.findViewById(R.id.tv_simple_memo);
|
||||
tvSimpleDistance = (TextView) itemView.findViewById(R.id.tv_simple_distance);
|
||||
ptlvSimpleTasks = (PositionTaskListView) itemView.findViewById(R.id.ptlv_simple_tasks);
|
||||
}
|
||||
}
|
||||
|
||||
public static class EditViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvEditLon;
|
||||
TextView tvEditLat;
|
||||
EditText etEditMemo;
|
||||
TextView tvEditDistance;
|
||||
RadioGroup rgDistanceSwitch;
|
||||
Button btnCancel;
|
||||
Button btnDelete;
|
||||
Button btnSave;
|
||||
Button btnAddTask;
|
||||
TextView tvTaskCount;
|
||||
PositionTaskListView ptlvEditTasks;
|
||||
|
||||
public EditViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
tvEditLon = (TextView) itemView.findViewById(R.id.tv_edit_longitude);
|
||||
tvEditLat = (TextView) itemView.findViewById(R.id.tv_edit_latitude);
|
||||
etEditMemo = (EditText) itemView.findViewById(R.id.et_edit_memo);
|
||||
tvEditDistance = (TextView) itemView.findViewById(R.id.tv_edit_distance);
|
||||
rgDistanceSwitch = (RadioGroup) itemView.findViewById(R.id.rg_distance_switch);
|
||||
btnCancel = (Button) itemView.findViewById(R.id.btn_edit_cancel);
|
||||
btnDelete = (Button) itemView.findViewById(R.id.btn_edit_delete);
|
||||
btnSave = (Button) itemView.findViewById(R.id.btn_edit_save);
|
||||
btnAddTask = (Button) itemView.findViewById(R.id.btn_add_task);
|
||||
tvTaskCount = (TextView) itemView.findViewById(R.id.tv_task_count);
|
||||
ptlvEditTasks = (PositionTaskListView) itemView.findViewById(R.id.ptlv_edit_tasks);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 方法说明类(保留不动)
|
||||
// =========================================================================
|
||||
public static class PositionTaskListViewRequiredMethods {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package cc.winboll.studio.positions.models;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/10/01 04:50
|
||||
* @Describe AppConfigsModel
|
||||
*/
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import android.util.JsonWriter;
|
||||
import android.util.JsonReader;
|
||||
import java.io.IOException;
|
||||
|
||||
public class AppConfigsModel extends BaseBean {
|
||||
|
||||
public static final String TAG = "AppConfigsModel";
|
||||
|
||||
boolean isEnableMainService;
|
||||
|
||||
public AppConfigsModel(boolean isEnableMainService) {
|
||||
this.isEnableMainService = isEnableMainService;
|
||||
}
|
||||
|
||||
public AppConfigsModel() {
|
||||
this.isEnableMainService = false;
|
||||
}
|
||||
|
||||
public void setIsEnableMainService(boolean isEnableMainService) {
|
||||
this.isEnableMainService = isEnableMainService;
|
||||
}
|
||||
|
||||
public boolean isEnableMainService() {
|
||||
return isEnableMainService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return AppConfigsModel.class.getName();
|
||||
}
|
||||
|
||||
// JSON序列化(保存位置数据)
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name("isEnableDistanceRefreshService").value(isEnableMainService());
|
||||
}
|
||||
|
||||
// JSON反序列化(加载位置数据,校验字段)
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
return true;
|
||||
} else {
|
||||
if (name.equals("isEnableDistanceRefreshService")) {
|
||||
setIsEnableMainService(jsonReader.nextBoolean());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 从JSON读取位置数据
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue(); // 跳过未知字段
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package cc.winboll.studio.positions.models;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/29 18:57
|
||||
* @Describe 位置数据模型
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PositionModel extends BaseBean {
|
||||
|
||||
public static final String TAG = "PositionModel";
|
||||
// 位置唯一标识符(与任务的positionId绑定)
|
||||
String positionId;
|
||||
// 经度(范围:-180~180)
|
||||
double longitude;
|
||||
// 纬度(范围:-90~90)
|
||||
double latitude;
|
||||
// 位置备注(空值时显示“无备注”)
|
||||
String memo;
|
||||
// 定位点与指定点实时距离长度
|
||||
double realPositionDistance;
|
||||
// 是否启用实时距离计算
|
||||
boolean isEnableRealPositionDistance;
|
||||
// 是否显示简单视图(true=简单视图,false=编辑视图)
|
||||
boolean isSimpleView = true;
|
||||
|
||||
// 带参构造(强制初始化位置ID和经纬度)
|
||||
public PositionModel(String positionId, double longitude, double latitude, String memo, boolean isEnableRealPositionDistance) {
|
||||
this.positionId = (positionId == null || positionId.trim().isEmpty()) ? genPositionId() : positionId;
|
||||
this.longitude = Math.max(-180, Math.min(180, longitude)); // 经度范围限制
|
||||
this.latitude = Math.max(-90, Math.min(90, latitude)); // 纬度范围限制
|
||||
this.memo = (memo == null || memo.trim().isEmpty()) ? "无备注" : memo;
|
||||
this.isEnableRealPositionDistance = isEnableRealPositionDistance;
|
||||
}
|
||||
|
||||
// 无参构造(默认值初始化,避免空指针)
|
||||
public PositionModel() {
|
||||
this.positionId = genPositionId();
|
||||
this.longitude = 0.0;
|
||||
this.latitude = 0.0;
|
||||
this.memo = "无备注";
|
||||
this.isEnableRealPositionDistance = false;
|
||||
}
|
||||
|
||||
public void setRealPositionDistance(double realPositionDistance) {
|
||||
this.realPositionDistance = realPositionDistance;
|
||||
}
|
||||
|
||||
public double getRealPositionDistance() {
|
||||
return realPositionDistance;
|
||||
}
|
||||
|
||||
// ---------------------- Getter/Setter(确保字段有效性) ----------------------
|
||||
public void setPositionId(String positionId) {
|
||||
this.positionId = (positionId == null || positionId.trim().isEmpty()) ? genPositionId() : positionId;
|
||||
}
|
||||
|
||||
public String getPositionId() {
|
||||
return positionId;
|
||||
}
|
||||
|
||||
public void setIsEnableRealPositionDistance(boolean isEnableRealPositionDistance) {
|
||||
this.isEnableRealPositionDistance = isEnableRealPositionDistance;
|
||||
}
|
||||
|
||||
public boolean isEnableRealPositionDistance() {
|
||||
return isEnableRealPositionDistance;
|
||||
}
|
||||
|
||||
public void setIsSimpleView(boolean isSimpleView) {
|
||||
this.isSimpleView = isSimpleView;
|
||||
}
|
||||
|
||||
public boolean isSimpleView() {
|
||||
return isSimpleView;
|
||||
}
|
||||
|
||||
public void setMemo(String memo) {
|
||||
this.memo = (memo == null || memo.trim().isEmpty()) ? "无备注" : memo;
|
||||
}
|
||||
|
||||
public String getMemo() {
|
||||
return memo;
|
||||
}
|
||||
|
||||
public void setLongitude(double longitude) {
|
||||
this.longitude = Math.max(-180, Math.min(180, longitude)); // 限制经度范围
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
|
||||
public void setLatitude(double latitude) {
|
||||
this.latitude = Math.max(-90, Math.min(90, latitude)); // 限制纬度范围
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
// ---------------------- 父类方法重写 ----------------------
|
||||
@Override
|
||||
public String getName() {
|
||||
return PositionModel.class.getName();
|
||||
}
|
||||
|
||||
// 生成唯一位置ID(与任务ID格式一致,确保关联匹配)
|
||||
public static String genPositionId() {
|
||||
UUID uniqueUuid = UUID.randomUUID();
|
||||
return uniqueUuid.toString(); // 36位标准UUID(含横杠,确保与任务ID格式统一)
|
||||
}
|
||||
|
||||
// JSON序列化(保存位置数据)
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name("positionId").value(getPositionId());
|
||||
jsonWriter.name("longitude").value(getLongitude());
|
||||
jsonWriter.name("latitude").value(getLatitude());
|
||||
jsonWriter.name("memo").value(getMemo());
|
||||
jsonWriter.name("isEnableRealPositionDistance").value(isEnableRealPositionDistance());
|
||||
}
|
||||
|
||||
// JSON反序列化(加载位置数据,校验字段)
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
return true;
|
||||
} else {
|
||||
if (name.equals("positionId")) {
|
||||
setPositionId(jsonReader.nextString());
|
||||
} else if (name.equals("longitude")) {
|
||||
setLongitude(jsonReader.nextDouble());
|
||||
} else if (name.equals("latitude")) {
|
||||
setLatitude(jsonReader.nextDouble());
|
||||
} else if (name.equals("memo")) {
|
||||
setMemo(jsonReader.nextString());
|
||||
} else if (name.equals("isEnableRealPositionDistance")) {
|
||||
setIsEnableRealPositionDistance(jsonReader.nextBoolean());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 从JSON读取位置数据
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue(); // 跳过未知字段
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
|
||||
// ---------------------- 核心工具方法:计算两点距离(Haversine公式,确保精度) ----------------------
|
||||
/**
|
||||
* 计算两个位置之间的直线距离(地球表面最短距离)
|
||||
* @param position1 第一个位置(非null)
|
||||
* @param position2 第二个位置(非null)
|
||||
* @param isKilometer 是否返回千米单位:true→千米,false→米
|
||||
* @return 距离(保留1位小数,符合显示需求)
|
||||
* @throws IllegalArgumentException 位置为null或经纬度无效时抛出
|
||||
*/
|
||||
public static double calculatePositionDistance(PositionModel position1, PositionModel position2, boolean isKilometer) {
|
||||
// 1. 校验参数有效性(避免计算异常)
|
||||
if (position1 == null || position2 == null) {
|
||||
throw new IllegalArgumentException("位置对象不能为null");
|
||||
}
|
||||
double lon1 = position1.getLongitude();
|
||||
double lat1 = position1.getLatitude();
|
||||
double lon2 = position2.getLongitude();
|
||||
double lat2 = position2.getLatitude();
|
||||
// 经纬度范围二次校验(确保有效)
|
||||
if (lat1 < -90 || lat1 > 90 || lat2 < -90 || lat2 > 90
|
||||
|| lon1 < -180 || lon1 > 180 || lon2 < -180 || lon2 > 180) {
|
||||
throw new IllegalArgumentException("经纬度值无效(纬度:-90~90,经度:-180~180)");
|
||||
}
|
||||
|
||||
// 2. Haversine公式计算(地球半径取6371km,行业标准)
|
||||
final double EARTH_RADIUS_KM = 6371;
|
||||
double radLat1 = Math.toRadians(lat1); // 角度转弧度
|
||||
double radLat2 = Math.toRadians(lat2);
|
||||
double radLon1 = Math.toRadians(lon1);
|
||||
double radLon2 = Math.toRadians(lon2);
|
||||
|
||||
double deltaLat = radLat2 - radLat1; // 纬度差
|
||||
double deltaLon = radLon2 - radLon1; // 经度差
|
||||
|
||||
// 核心公式
|
||||
double a = Math.pow(Math.sin(deltaLat / 2), 2)
|
||||
+ Math.cos(radLat1) * Math.cos(radLat2)
|
||||
* Math.pow(Math.sin(deltaLon / 2), 2);
|
||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
double distanceKm = EARTH_RADIUS_KM * c; // 距离(千米)
|
||||
|
||||
// 3. 单位转换+精度处理(保留1位小数,符合显示需求)
|
||||
double distance;
|
||||
if (isKilometer) {
|
||||
distance = Math.round(distanceKm * 10.0) / 10.0; // 千米→1位小数
|
||||
} else {
|
||||
distance = Math.round(distanceKm * 1000 * 10.0) / 10.0; // 米→1位小数
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
package cc.winboll.studio.positions.models;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/30 02:48
|
||||
* @Describe 位置任务数据模型
|
||||
*/
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import cc.winboll.studio.libappbase.BaseBean;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PositionTaskModel extends BaseBean {
|
||||
|
||||
public static final String TAG = "PositionTaskModel";
|
||||
// 任务标识符(唯一)
|
||||
String taskId;
|
||||
// 绑定的位置标识符(与PositionModel的positionId一一对应)
|
||||
String positionId;
|
||||
// 任务描述
|
||||
String taskDescription;
|
||||
// 任务距离条件:是否大于设定距离
|
||||
boolean isGreaterThan;
|
||||
// 任务距离条件:是否小于设定距离(与isGreaterThan互斥)
|
||||
boolean isLessThan;
|
||||
// 任务条件距离(单位:米)
|
||||
int discussDistance;
|
||||
// 任务开始启用时间
|
||||
long startTime;
|
||||
// 任务是否已触发
|
||||
boolean isBingo = false;
|
||||
// 是否启用任务
|
||||
boolean isEnable;
|
||||
|
||||
// 带参构造(强制传入positionId,确保任务与位置绑定)
|
||||
public PositionTaskModel(String taskId, String positionId, String taskDescription, boolean isGreaterThan, int discussDistance, long startTime, boolean isEnable) {
|
||||
this.taskId = (taskId == null || taskId.trim().isEmpty()) ? genTaskId() : taskId; // 空ID自动生成
|
||||
this.positionId = positionId; // 强制绑定位置ID
|
||||
this.taskDescription = (taskDescription == null || taskDescription.trim().isEmpty()) ? "新任务" : taskDescription;
|
||||
this.isGreaterThan = isGreaterThan;
|
||||
this.isLessThan = !isGreaterThan; // 确保互斥
|
||||
this.discussDistance = Math.max(discussDistance, 1); // 距离最小1米,避免无效值
|
||||
this.startTime = startTime;
|
||||
this.isEnable = isEnable;
|
||||
}
|
||||
|
||||
// 无参构造(初始化默认值,positionId需后续设置)
|
||||
public PositionTaskModel() {
|
||||
this.taskId = genTaskId();
|
||||
this.positionId = "";
|
||||
this.taskDescription = "新任务";
|
||||
this.isGreaterThan = true;
|
||||
this.isLessThan = false; // 初始互斥
|
||||
this.discussDistance = 100; // 默认100米
|
||||
this.startTime = System.currentTimeMillis();
|
||||
this.isEnable = true;
|
||||
}
|
||||
|
||||
public void setStartTime(long startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public long getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setIsBingo(boolean isBingo) {
|
||||
this.isBingo = isBingo;
|
||||
}
|
||||
|
||||
public boolean isBingo() {
|
||||
return isBingo;
|
||||
}
|
||||
|
||||
// ---------------------- Getter/Setter(确保positionId不为空,距离有效) ----------------------
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = (taskId == null || taskId.trim().isEmpty()) ? genTaskId() : taskId;
|
||||
}
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setPositionId(String positionId) {
|
||||
this.positionId = (positionId == null || positionId.trim().isEmpty()) ? "" : positionId; // 空值防护
|
||||
}
|
||||
|
||||
public String getPositionId() {
|
||||
return positionId;
|
||||
}
|
||||
|
||||
public void setTaskDescription(String taskDescription) {
|
||||
this.taskDescription = (taskDescription == null || taskDescription.trim().isEmpty()) ? "新任务" : taskDescription;
|
||||
}
|
||||
|
||||
public String getTaskDescription() {
|
||||
return taskDescription;
|
||||
}
|
||||
|
||||
// 修复:确保isGreaterThan和isLessThan互斥
|
||||
public void setIsGreaterThan(boolean isGreaterThan) {
|
||||
this.isGreaterThan = isGreaterThan;
|
||||
this.isLessThan = !isGreaterThan; // 关键:小于 = 非大于
|
||||
}
|
||||
|
||||
public boolean isGreaterThan() {
|
||||
return isGreaterThan;
|
||||
}
|
||||
|
||||
// 修复:确保isLessThan和isGreaterThan互斥
|
||||
public void setIsLessThan(boolean isLessThan) {
|
||||
this.isLessThan = isLessThan;
|
||||
this.isGreaterThan = !isLessThan; // 关键:大于 = 非小于
|
||||
}
|
||||
|
||||
public boolean isLessThan() {
|
||||
return isLessThan;
|
||||
}
|
||||
|
||||
public void setDiscussDistance(int discussDistance) {
|
||||
this.discussDistance = Math.max(discussDistance, 1); // 距离最小1米,避免0或负数
|
||||
}
|
||||
|
||||
public int getDiscussDistance() {
|
||||
return discussDistance;
|
||||
}
|
||||
|
||||
public void setIsEnable(boolean isEnable) {
|
||||
this.isEnable = isEnable;
|
||||
}
|
||||
|
||||
public boolean isEnable() {
|
||||
return isEnable;
|
||||
}
|
||||
|
||||
// ---------------------- 父类方法重写 ----------------------
|
||||
@Override
|
||||
public String getName() {
|
||||
return PositionTaskModel.class.getName();
|
||||
}
|
||||
|
||||
// 生成唯一任务ID(与PositionModel保持一致格式)
|
||||
public static String genTaskId() {
|
||||
UUID uniqueUuid = UUID.randomUUID();
|
||||
return uniqueUuid.toString(); // 36位标准UUID(含横杠,确保唯一)
|
||||
}
|
||||
|
||||
// JSON序列化(保存任务数据,包含所有字段)
|
||||
@Override
|
||||
public void writeThisToJsonWriter(JsonWriter jsonWriter) throws IOException {
|
||||
super.writeThisToJsonWriter(jsonWriter);
|
||||
jsonWriter.name("taskId").value(getTaskId());
|
||||
jsonWriter.name("positionId").value(getPositionId());
|
||||
jsonWriter.name("taskDescription").value(getTaskDescription());
|
||||
jsonWriter.name("isGreaterThan").value(isGreaterThan());
|
||||
jsonWriter.name("isLessThan").value(isLessThan());
|
||||
jsonWriter.name("discussDistance").value(getDiscussDistance());
|
||||
jsonWriter.name("startTime").value(getStartTime());
|
||||
jsonWriter.name("isEnable").value(isEnable());
|
||||
}
|
||||
|
||||
// JSON反序列化(加载任务数据,校验字段有效性)
|
||||
@Override
|
||||
public boolean initObjectsFromJsonReader(JsonReader jsonReader, String name) throws IOException {
|
||||
if (super.initObjectsFromJsonReader(jsonReader, name)) {
|
||||
return true;
|
||||
} else {
|
||||
if (name.equals("taskId")) {
|
||||
setTaskId(jsonReader.nextString());
|
||||
} else if (name.equals("positionId")) {
|
||||
setPositionId(jsonReader.nextString());
|
||||
} else if (name.equals("taskDescription")) {
|
||||
setTaskDescription(jsonReader.nextString());
|
||||
} else if (name.equals("isGreaterThan")) {
|
||||
setIsGreaterThan(jsonReader.nextBoolean());
|
||||
} else if (name.equals("isLessThan")) {
|
||||
setIsLessThan(jsonReader.nextBoolean());
|
||||
} else if (name.equals("discussDistance")) {
|
||||
setDiscussDistance(jsonReader.nextInt());
|
||||
} else if (name.equals("startTime")) {
|
||||
setStartTime(jsonReader.nextLong());
|
||||
} else if (name.equals("isEnable")) {
|
||||
setIsEnable(jsonReader.nextBoolean());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 从JSON读取任务数据(确保反序列化完整)
|
||||
@Override
|
||||
public BaseBean readBeanFromJsonReader(JsonReader jsonReader) throws IOException {
|
||||
jsonReader.beginObject();
|
||||
while (jsonReader.hasNext()) {
|
||||
String name = jsonReader.nextName();
|
||||
if (!initObjectsFromJsonReader(jsonReader, name)) {
|
||||
jsonReader.skipValue(); // 跳过未知字段,避免崩溃
|
||||
}
|
||||
}
|
||||
jsonReader.endObject();
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package cc.winboll.studio.positions.services;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/19 14:30:57
|
||||
* @Describe 应用主要服务组件类守护进程服务组件类
|
||||
*/
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.IBinder;
|
||||
import cc.winboll.studio.positions.services.MainService;
|
||||
import cc.winboll.studio.positions.utils.AppConfigsUtil;
|
||||
import cc.winboll.studio.positions.utils.ServiceUtil;
|
||||
|
||||
public class AssistantService extends Service {
|
||||
|
||||
public final static String TAG = "AssistantService";
|
||||
public static final String EXTRA_IS_SETTING_TO_ENABLE = "EXTRA_IS_SETTING_TO_ENABLE";
|
||||
|
||||
MyServiceConnection mMyServiceConnection;
|
||||
volatile boolean mIsServiceRunning;
|
||||
AppConfigsUtil mAppConfigsUtil;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mAppConfigsUtil = AppConfigsUtil.getInstance(this);
|
||||
if (mMyServiceConnection == null) {
|
||||
mMyServiceConnection = new MyServiceConnection();
|
||||
}
|
||||
// 设置运行参数
|
||||
mIsServiceRunning = false;
|
||||
if (mAppConfigsUtil.isEnableMainService(true)) {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (mAppConfigsUtil.isEnableMainService(true)) {
|
||||
run();
|
||||
}
|
||||
|
||||
return mAppConfigsUtil.isEnableMainService(true) ? Service.START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
mIsServiceRunning = false;
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
//
|
||||
// 运行服务内容
|
||||
//
|
||||
void run() {
|
||||
if (mAppConfigsUtil.isEnableMainService(true)) {
|
||||
if (mIsServiceRunning == false) {
|
||||
// 设置运行状态
|
||||
mIsServiceRunning = true;
|
||||
// 唤醒和绑定主进程
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 唤醒和绑定主进程
|
||||
//
|
||||
void wakeupAndBindMain() {
|
||||
if (ServiceUtil.isServiceAlive(getApplicationContext(), MainService.class.getName()) == false) {
|
||||
startForegroundService(new Intent(AssistantService.this, MainService.class));
|
||||
}
|
||||
|
||||
bindService(new Intent(AssistantService.this, MainService.class), mMyServiceConnection, Context.BIND_IMPORTANT);
|
||||
}
|
||||
|
||||
//
|
||||
// 主进程与守护进程连接时需要用到此类
|
||||
//
|
||||
class MyServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
if (mAppConfigsUtil.isEnableMainService(true)) {
|
||||
wakeupAndBindMain();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
//package cc.winboll.studio.positions.services;
|
||||
//
|
||||
///**
|
||||
// * @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
// * @Date 2025/09/30 19:53
|
||||
// * @Describe 位置距离服务:管理数据+定时计算距离+适配Adapter(Java 7 兼容)+ GPS信号加载
|
||||
// */
|
||||
//import android.app.Service;
|
||||
//import android.content.Context;
|
||||
//import android.content.Intent;
|
||||
//import android.content.pm.PackageManager;
|
||||
//import android.location.Location;
|
||||
//import android.location.LocationListener;
|
||||
//import android.location.LocationManager;
|
||||
//import android.os.Binder;
|
||||
//import android.os.Build;
|
||||
//import android.os.Bundle;
|
||||
//import android.os.IBinder;
|
||||
//import android.os.Looper;
|
||||
//import android.widget.Toast;
|
||||
//import cc.winboll.studio.libappbase.LogUtils;
|
||||
//import cc.winboll.studio.positions.adapters.PositionAdapter;
|
||||
//import cc.winboll.studio.positions.models.AppConfigsModel;
|
||||
//import cc.winboll.studio.positions.models.PositionModel;
|
||||
//import cc.winboll.studio.positions.models.PositionTaskModel;
|
||||
//import cc.winboll.studio.positions.utils.NotificationUtil;
|
||||
//import java.util.ArrayList;
|
||||
//import java.util.HashSet;
|
||||
//import java.util.Iterator;
|
||||
//import java.util.Set;
|
||||
//import java.util.concurrent.Executors;
|
||||
//import java.util.concurrent.ScheduledExecutorService;
|
||||
//import java.util.concurrent.TimeUnit;
|
||||
//
|
||||
///**
|
||||
// * 核心职责:
|
||||
// * 1. 实现 PositionAdapter.DistanceServiceInterface 接口,解耦Adapter与服务
|
||||
// * 2. 单例式管理位置/任务数据,提供安全增删改查接口
|
||||
// * 3. 后台单线程定时计算可见位置距离,主线程回调更新UI
|
||||
// * 4. 内置GPS信号加载(通过LocationManager实时获取位置,解决“等待GPS信号”问题)
|
||||
// * 5. 服务启动时启动前台通知(保活后台GPS功能,符合系统规范)
|
||||
// * 6. 严格Java 7语法:无Lambda/Stream,显式迭代器/匿名内部类
|
||||
// */
|
||||
//public class DistanceRefreshService extends Service {
|
||||
// public static final String TAG = "DistanceRefreshService";
|
||||
//
|
||||
//
|
||||
// // 服务状态与配置
|
||||
// private boolean isServiceRunning = false;
|
||||
//
|
||||
// private static final int REFRESH_INTERVAL = 3; // 距离刷新间隔(秒)
|
||||
// // 前台通知相关:记录是否已启动前台服务(避免重复调用startForeground)
|
||||
// private boolean isForegroundServiceStarted = false;
|
||||
//
|
||||
//
|
||||
//
|
||||
// // 服务绑定与UI回调
|
||||
// private final IBinder mBinder = new DistanceBinder();
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
// /**
|
||||
// * 在主线程显示Toast(避免子线程无法显示Toast的问题)
|
||||
// */
|
||||
// private void showToastOnMainThread(final String message) {
|
||||
// if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
// Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||
// } else {
|
||||
// new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// Toast.makeText(DistanceRefreshService.this, message, Toast.LENGTH_SHORT).show();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // ---------------------- Binder 内部类(供外部绑定服务) ----------------------
|
||||
// public class DistanceBinder extends Binder {
|
||||
// /**
|
||||
// * 外部绑定后获取服务实例(安全暴露服务引用)
|
||||
// */
|
||||
// public DistanceRefreshService getService() {
|
||||
// return DistanceRefreshService.this;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// @Override
|
||||
// public void onCreate() {
|
||||
// super.onCreate();
|
||||
//
|
||||
// // 初始化GPS管理器(提前获取系统服务,避免启动时延迟)
|
||||
// mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
||||
// LogUtils.d(TAG, "服务 onCreate:初始化完成,等待启动命令");
|
||||
// run();
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
// run();
|
||||
// AppConfigsModel bean = AppConfigsModel.loadBean(DistanceRefreshService.this, AppConfigsModel.class);
|
||||
// boolean isEnableService = (bean == null) ? false : bean.isEnableMainService();
|
||||
// // 服务启用时返回START_STICKY(被杀死后尝试重启),禁用时返回默认值
|
||||
// return isEnableService ? Service.START_STICKY : super.onStartCommand(intent, flags, startId);
|
||||
// }
|
||||
//
|
||||
// public void run() {
|
||||
// // 仅服务未运行时启动(避免重复启动)
|
||||
// if (!isServiceRunning) {
|
||||
// isServiceRunning = true;
|
||||
//
|
||||
//
|
||||
//
|
||||
// startDistanceRefreshTask(); // 启动定时距离计算
|
||||
// startForegroundNotification(); // 启动前台通知
|
||||
//
|
||||
// LogUtils.d(TAG, "服务 onStartCommand:启动成功,刷新间隔=" + REFRESH_INTERVAL + "秒,前台通知+GPS已启动");
|
||||
// } else {
|
||||
// LogUtils.w(TAG, "服务 onStartCommand:已在运行,无需重复启动(前台通知:" + (isForegroundServiceStarted ? "已启动" : "未启动") + " | GPS:" + (isGpsEnabled ? "已开启" : "未开启") + ")");
|
||||
// // 异常场景恢复:补全未启动的组件
|
||||
// if (!isForegroundServiceStarted) {
|
||||
// startForegroundNotification();
|
||||
// LogUtils.d(TAG, "服务 run:前台通知未启动,已恢复");
|
||||
// }
|
||||
// if (isServiceRunning && !isGpsEnabled) {
|
||||
// startGpsLocation();
|
||||
// LogUtils.d(TAG, "服务 run:GPS未启动,已恢复");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public IBinder onBind(Intent intent) {
|
||||
// return null; // 按你的业务逻辑返回,无绑定需求则保留null
|
||||
// //LogUtils.d(TAG, "服务 onBind:外部绑定成功(运行状态:" + (isServiceRunning ? "是" : "否") + " | GPS状态:" + (isGpsEnabled ? "可用" : "不可用") + ")");
|
||||
// //return mBinder; // 返回Binder实例,供外部获取服务
|
||||
// }
|
||||
//
|
||||
// /*@Override
|
||||
// public boolean onUnbind(Intent intent) {
|
||||
// LogUtils.d(TAG, "服务 onUnbind:外部解绑,清理回调与可见位置");
|
||||
// // 解绑后清理资源,避免内存泄漏
|
||||
// mDistanceReceiver = null;
|
||||
// mVisiblePositionIds.clear();
|
||||
// // 解绑时不停止GPS(服务仍在后台运行,需持续获取位置)
|
||||
// return super.onUnbind(intent);
|
||||
// }*/
|
||||
//
|
||||
// @Override
|
||||
// public void onDestroy() {
|
||||
// super.onDestroy();
|
||||
//
|
||||
// LogUtils.d(TAG, "服务 onDestroy:销毁完成,资源已释放(GPS+前台通知+线程池)");
|
||||
// }
|
||||
//
|
||||
// // ---------------------- 前台服务通知管理(与GPS状态联动优化) ----------------------
|
||||
// /**
|
||||
// * 启动前台服务通知(调用NotificationUtils创建通知,确保仅启动一次)
|
||||
// */
|
||||
// private void startForegroundNotification() {
|
||||
// // 1. 校验:避免重复调用startForeground(系统不允许重复启动)
|
||||
// if (isForegroundServiceStarted) {
|
||||
// LogUtils.w(TAG, "startForegroundNotification:前台通知已启动,无需重复执行");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
//// 2. 初始化通知状态文本(根据GPS初始状态动态显示,避免固定“等待”)
|
||||
// String initialStatus;
|
||||
// if (isGpsPermissionGranted && isGpsEnabled) {
|
||||
// initialStatus = "GPS已就绪,正在获取位置(刷新间隔" + REFRESH_INTERVAL + "秒)";
|
||||
// } else if (!isGpsPermissionGranted) {
|
||||
// initialStatus = "缺少定位权限,无法获取GPS位置";
|
||||
// } else {
|
||||
// initialStatus = "GPS未开启,请在设置中打开";
|
||||
// }
|
||||
//
|
||||
//
|
||||
//// 5. 标记前台服务已启动
|
||||
// isForegroundServiceStarted = true;
|
||||
// LogUtils.d(TAG, "startForegroundNotification:前台服务通知启动成功,初始状态:" + initialStatus);
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
//// 捕获异常(如上下文失效、通知渠道未创建)
|
||||
// isForegroundServiceStarted = false;
|
||||
// LogUtils.d(TAG, "startForegroundNotification:前台通知启动失败" + e);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// /**
|
||||
//
|
||||
// - 主线程回调Adapter更新UI(避免跨线程操作UI异常)
|
||||
// */
|
||||
// /*private void notifyDistanceUpdateToUI(final String positionId) {
|
||||
// if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
// if (mDistanceReceiver != null) {
|
||||
// mDistanceReceiver.onDistanceUpdate(positionId);
|
||||
// }
|
||||
// } else {
|
||||
// new android.os.Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// if (mDistanceReceiver != null) {
|
||||
// mDistanceReceiver.onDistanceUpdate(positionId);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }*/
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//// ---------------------- 实现 PositionAdapter.DistanceServiceInterface 接口 ----------------------
|
||||
//
|
||||
// public ArrayList getPositionList() {
|
||||
// if (!isServiceRunning) {
|
||||
// LogUtils.w(TAG, "getPositionList:服务未运行,返回空列表");
|
||||
// return new ArrayList();
|
||||
// }
|
||||
// return new ArrayList(mPositionList);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// public ArrayList getPositionTasksList() {
|
||||
// if (!isServiceRunning) {
|
||||
// LogUtils.w(TAG, "getPositionTasksList:服务未运行,返回空列表");
|
||||
// return new ArrayList();
|
||||
// }
|
||||
// return new ArrayList(mTaskList);
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
// /*public void setOnDistanceUpdateReceiver(PositionAdapter.OnDistanceUpdateReceiver receiver) {
|
||||
// this.mDistanceReceiver = receiver;
|
||||
// LogUtils.d(TAG, "setOnDistanceUpdateReceiver:回调接收器已设置(" + (receiver != null ? "有效" : "无效") + ")");
|
||||
// }*/
|
||||
//
|
||||
// public void addVisibleDistanceView(String positionId) {
|
||||
// if (!isServiceRunning || positionId == null) {
|
||||
// LogUtils.w(TAG, "addVisibleDistanceView:服务未运行/位置ID无效,添加失败");
|
||||
// return;
|
||||
// }
|
||||
// if (mVisiblePositionIds.add(positionId)) {
|
||||
// LogUtils.d(TAG, "addVisibleDistanceView:添加成功(位置ID=" + positionId + ",当前可见数=" + mVisiblePositionIds.size() + ")");
|
||||
//// 新增:添加可见位置后,立即更新通知(显示最新可见数量)
|
||||
// if (isForegroundServiceStarted && mCurrentGpsPosition != null) {
|
||||
// syncGpsStatusToNotification();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public void removeVisibleDistanceView(String positionId) {
|
||||
// if (positionId == null) {
|
||||
// LogUtils.w(TAG, "removeVisibleDistanceView:位置ID为空,移除失败");
|
||||
// return;
|
||||
// }
|
||||
// if (mVisiblePositionIds.remove(positionId)) {
|
||||
// int remainingCount = mVisiblePositionIds.size();
|
||||
// LogUtils.d(TAG, "removeVisibleDistanceView:移除成功(位置ID=" + positionId + ",当前可见数=" + remainingCount + ")");
|
||||
//// 新增:移除可见位置后,更新通知(同步数量变化)
|
||||
// if (isForegroundServiceStarted && mCurrentGpsPosition != null) {
|
||||
// syncGpsStatusToNotification();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public void clearVisibleDistanceViews() {
|
||||
// mVisiblePositionIds.clear();
|
||||
// LogUtils.d(TAG, "clearVisibleDistanceViews:所有可见位置已清空");
|
||||
//// 新增:清空可见位置后,更新通知(提示计算暂停)
|
||||
// if (isForegroundServiceStarted) {
|
||||
// updateNotificationGpsStatus("无可见位置,距离计算暂停");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//// ---------------------- 数据管理接口(修复原有语法错误+优化逻辑) ----------------------
|
||||
// /**
|
||||
//
|
||||
// - 获取服务运行状态
|
||||
// */
|
||||
// public boolean isServiceRunning() {
|
||||
// return isServiceRunning;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
//
|
||||
// - 添加位置(修复迭代器泛型缺失问题)
|
||||
// */
|
||||
// public void addPosition(PositionModel position) {
|
||||
// if (!isServiceRunning || position == null || position.getPositionId() == null) {
|
||||
// LogUtils.w(TAG, "addPosition:服务未运行/数据无效,添加失败");
|
||||
// return;
|
||||
// }// 修复:显式声明PositionModel泛型,避免类型转换警告
|
||||
// boolean isDuplicate = false;
|
||||
// Iterator posIter = mPositionList.iterator();
|
||||
// while (posIter.hasNext()) {
|
||||
// PositionModel existingPos = (PositionModel)posIter.next();
|
||||
// if (position.getPositionId().equals(existingPos.getPositionId())) {
|
||||
// isDuplicate = true;
|
||||
// break;
|
||||
// }
|
||||
// }if (!isDuplicate) {
|
||||
// mPositionList.add(position);
|
||||
// LogUtils.d(TAG, "addPosition:添加成功(位置ID=" + position.getPositionId() + ",总数=" + mPositionList.size() + ")");
|
||||
// } else {
|
||||
// LogUtils.w(TAG, "addPosition:位置ID=" + position.getPositionId() + "已存在,添加失败");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
//
|
||||
// - 删除位置(修复任务删除时的类型转换错误)
|
||||
// */
|
||||
// public void removePosition(String positionId) {
|
||||
// if (!isServiceRunning || positionId == null) {
|
||||
// LogUtils.w(TAG, "removePosition:服务未运行/位置ID无效,删除失败");
|
||||
// return;
|
||||
// }// 1. 删除位置
|
||||
// boolean isRemoved = false;
|
||||
// Iterator posIter = mPositionList.iterator();
|
||||
// while (posIter.hasNext()) {
|
||||
// PositionModel pos = (PositionModel)posIter.next();
|
||||
// if (positionId.equals(pos.getPositionId())) {
|
||||
// posIter.remove();
|
||||
// isRemoved = true;
|
||||
// break;
|
||||
// }
|
||||
// }if (isRemoved) {
|
||||
//// 修复:任务列表迭代时用PositionTaskModel泛型(原错误用PositionModel导致转换失败)
|
||||
// Iterator taskIter = mTaskList.iterator();
|
||||
// while (taskIter.hasNext()) {
|
||||
// PositionTaskModel task = (PositionTaskModel)taskIter.next();
|
||||
// if (positionId.equals(task.getPositionId())) {
|
||||
// taskIter.remove();
|
||||
// }
|
||||
// }// 3. 移除可见位置
|
||||
// mVisiblePositionIds.remove(positionId);
|
||||
// LogUtils.d(TAG, "removePosition:删除成功(位置ID=" + positionId + ",剩余位置数=" + mPositionList.size() + ",剩余任务数=" + mTaskList.size() + ")");
|
||||
// } else {
|
||||
// LogUtils.w(TAG, "removePosition:位置ID=" + positionId + "不存在,删除失败");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
//
|
||||
// - 更新位置信息(修复代码格式+迭代器泛型)
|
||||
// */
|
||||
// public void updatePosition(PositionModel updatedPosition) {
|
||||
// if (!isServiceRunning || updatedPosition == null || updatedPosition.getPositionId() == null) {
|
||||
// LogUtils.w(TAG, "updatePosition:服务未运行/数据无效,更新失败");
|
||||
// return;
|
||||
// }boolean isUpdated = false;
|
||||
// Iterator posIter = mPositionList.iterator();
|
||||
// while (posIter.hasNext()) {
|
||||
// PositionModel pos = (PositionModel)posIter.next();
|
||||
// if (updatedPosition.getPositionId().equals(pos.getPositionId())) {
|
||||
// pos.setMemo(updatedPosition.getMemo());
|
||||
// pos.setIsEnableRealPositionDistance(updatedPosition.isEnableRealPositionDistance());
|
||||
// if (!updatedPosition.isEnableRealPositionDistance()) {
|
||||
// pos.setRealPositionDistance(-1);
|
||||
// //notifyDistanceUpdateToUI(pos.getPositionId());
|
||||
// }
|
||||
// isUpdated = true;
|
||||
// break;
|
||||
// }
|
||||
// }if (isUpdated) {
|
||||
// LogUtils.d(TAG, "updatePosition:更新成功(位置ID=" + updatedPosition.getPositionId() + ")");
|
||||
// } else {
|
||||
// LogUtils.w(TAG, "updatePosition:位置ID=" + updatedPosition.getPositionId() + "不存在,更新失败");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
//
|
||||
// - 同步任务列表(修复泛型缺失+代码格式)
|
||||
// */
|
||||
// public void syncAllPositionTasks(ArrayList tasks) {
|
||||
// if (!isServiceRunning || tasks == null) {
|
||||
// LogUtils.w(TAG, "syncAllPositionTasks:服务未运行/任务列表为空,同步失败");
|
||||
// return;
|
||||
// }// 1. 清空旧任务
|
||||
// mTaskList.clear();
|
||||
//// 2. 添加新任务(修复泛型+去重逻辑)
|
||||
// Set taskIdSet = new HashSet();
|
||||
// Iterator taskIter = tasks.iterator();
|
||||
// while (taskIter.hasNext()) {
|
||||
// PositionTaskModel task = (PositionTaskModel)taskIter.next();
|
||||
// if (task != null && task.getTaskId() != null && !taskIdSet.contains(task.getTaskId())) {
|
||||
// taskIdSet.add(task.getTaskId());
|
||||
// mTaskList.add(task);
|
||||
// }
|
||||
// }LogUtils.d(TAG, "syncAllPositionTasks:同步成功(接收任务数=" + tasks.size() + ",去重后=" + mTaskList.size() + ")");
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//// ---------------------- 补充:修复LocationProvider引用缺失问题(避免编译错误) ----------------------
|
||||
//// 注:原代码中onStatusChanged使用LocationProvider枚举,需补充静态导入或显式声明
|
||||
//// 此处通过内部静态类定义,解决系统API引用问题(兼容Java 7语法)
|
||||
// private static class LocationProvider {
|
||||
// public static final int AVAILABLE = 2;
|
||||
// public static final int OUT_OF_SERVICE = 0;
|
||||
// public static final int TEMPORARILY_UNAVAILABLE = 1;
|
||||
// }
|
||||
//
|
||||
//// ---------------------- 补充:Context引用工具(避免服务销毁后Context失效) ----------------------
|
||||
// /*private Context getSafeContext() {
|
||||
// // 服务未销毁时返回自身Context,已销毁时返回应用Context(避免内存泄漏)
|
||||
// if (isDestroyed()) {
|
||||
// return getApplicationContext();
|
||||
// }
|
||||
// return this;
|
||||
// }*/
|
||||
//
|
||||
//// 注:isDestroyed()为API 17+方法,若需兼容更低版本,可添加版本判断
|
||||
// /*private boolean isDestroyed() {
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
// return super.isDestroyed();
|
||||
// }
|
||||
// // 低版本通过状态标记间接判断(服务销毁时会置为false)
|
||||
// return !isServiceRunning && !isForegroundServiceStarted;
|
||||
// }*/
|
||||
//}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
package cc.winboll.studio.positions.utils;
|
||||
import android.content.Context;
|
||||
import cc.winboll.studio.positions.models.AppConfigsModel;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/10/01 12:15
|
||||
* @Describe AppConfigsUtils
|
||||
*/
|
||||
public class AppConfigsUtil {
|
||||
|
||||
public static final String TAG = "AppConfigsUtils";
|
||||
|
||||
// 1. 私有静态成员变量(volatile 防止指令重排,确保实例初始化完全)
|
||||
private static volatile AppConfigsUtil sInstance;
|
||||
Context mContext;
|
||||
AppConfigsModel mAppConfigsModel;
|
||||
|
||||
// 2. 私有构造方法(禁止外部 new 实例)
|
||||
private AppConfigsUtil(Context context) {
|
||||
// 可选:防止通过反射创建实例(增强单例安全性)
|
||||
if (sInstance != null) {
|
||||
throw new RuntimeException("禁止通过反射创建单例实例");
|
||||
}
|
||||
mContext = context;
|
||||
loadConfigs();
|
||||
}
|
||||
|
||||
// 3. 公开静态方法(双重校验锁,获取唯一实例)
|
||||
public static AppConfigsUtil getInstance(Context context) {
|
||||
// 第一次校验:无锁,快速判断实例是否存在(提升性能)
|
||||
if (sInstance == null) {
|
||||
// 加锁:确保同一时间只有一个线程进入初始化逻辑
|
||||
synchronized (AppConfigsUtil.class) {
|
||||
// 第二次校验:防止多线程并发时重复创建实例(线程安全)
|
||||
if (sInstance == null) {
|
||||
sInstance = new AppConfigsUtil(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public void loadConfigs() {
|
||||
mAppConfigsModel = AppConfigsModel.loadBean(mContext, AppConfigsModel.class);
|
||||
}
|
||||
|
||||
public void saveConfigs() {
|
||||
AppConfigsModel.saveBean(mContext, mAppConfigsModel);
|
||||
}
|
||||
|
||||
public boolean isEnableMainService(boolean isReloadConfigs) {
|
||||
if (isReloadConfigs) {
|
||||
loadConfigs();
|
||||
}
|
||||
|
||||
return (mAppConfigsModel == null) ?false: mAppConfigsModel.isEnableMainService();
|
||||
}
|
||||
|
||||
public void setIsEnableMainService(boolean isEnableMainService) {
|
||||
if(mAppConfigsModel == null) {
|
||||
mAppConfigsModel = new AppConfigsModel();
|
||||
}
|
||||
mAppConfigsModel.setIsEnableMainService(isEnableMainService);
|
||||
saveConfigs();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package cc.winboll.studio.positions.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/10/22 01:16
|
||||
* @Describe DensityUtils
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.WindowManager;
|
||||
|
||||
/**
|
||||
* 屏幕密度工具类(dp/sp 转 px、获取屏幕尺寸)
|
||||
*/
|
||||
public class DensityUtils {
|
||||
public static final String TAG = "DensityUtils";
|
||||
|
||||
/**
|
||||
* dp 转 px(根据屏幕密度)
|
||||
*/
|
||||
public static int dp2px(Context context, float dpValue) {
|
||||
final float scale = context.getResources().getDisplayMetrics().density;
|
||||
return (int) (dpValue * scale + 0.5f);
|
||||
}
|
||||
|
||||
/**
|
||||
* sp 转 px(根据文字缩放比例)
|
||||
*/
|
||||
public static int sp2px(Context context, float spValue) {
|
||||
final float scale = context.getResources().getDisplayMetrics().scaledDensity;
|
||||
return (int) (spValue * scale + 0.5f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取屏幕宽度(像素)
|
||||
*/
|
||||
public static int getScreenWidth(Context context) {
|
||||
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||||
DisplayMetrics dm = new DisplayMetrics();
|
||||
wm.getDefaultDisplay().getMetrics(dm);
|
||||
return dm.widthPixels;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
package cc.winboll.studio.positions.utils;
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.libappbase.ToastUtils;
|
||||
import cc.winboll.studio.positions.models.PositionModel;
|
||||
import cc.winboll.studio.positions.models.PositionTaskModel;
|
||||
import cc.winboll.studio.positions.services.MainService;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/10/27 18:40
|
||||
* @Describe 距离计算工具集(单例模式)
|
||||
*/
|
||||
public class DistanceCalculatorUtil {
|
||||
|
||||
public static final String TAG = "DistanceCalculatorUtil";
|
||||
|
||||
// 1. 私有静态 volatile 实例:保证多线程下实例可见性,避免指令重排序导致的空指针
|
||||
private static volatile DistanceCalculatorUtil sInstance;
|
||||
Context mContext;
|
||||
ArrayList<PositionModel> mPositionList; // 位置数据列表
|
||||
ArrayList<PositionTaskModel> mAllTasks;// 任务数据列表
|
||||
PositionModel mGpsPositionCalculated;
|
||||
long mLastCalculatedTime = 0;
|
||||
long mMinCalculatedTimeBettween = 30000; // GPS数据更新时,两次计算之间的最小时间间隔
|
||||
double mMinjumpDistance = 10.0f; // GPS数据更新时,能跳跃距离最小有效值,达到有效值时,两次计算之间的最小时间间隔阀值将被忽略。
|
||||
|
||||
// 2. 私有构造器:禁止外部通过 new 关键字创建实例,确保单例唯一性
|
||||
private DistanceCalculatorUtil(Context context) {
|
||||
// 可选:初始化工具类依赖的资源(如配置参数、缓存等)
|
||||
mContext = context;
|
||||
|
||||
LogUtils.d(TAG, "DistanceCalculatorUtil 单例实例初始化");
|
||||
}
|
||||
|
||||
// 3. 公开静态方法:双重校验锁获取单例,兼顾线程安全与性能
|
||||
public static DistanceCalculatorUtil getInstance(Context context) {
|
||||
// 第一重校验:避免已创建实例时的频繁加锁,提升性能
|
||||
if (sInstance == null) {
|
||||
// 加锁:确保多线程下仅一个线程进入实例创建逻辑
|
||||
synchronized (DistanceCalculatorUtil.class) {
|
||||
// 第二重校验:防止多线程并发时重复创建实例
|
||||
if (sInstance == null) {
|
||||
sInstance = new DistanceCalculatorUtil(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
// ---------------------- 以下可补充距离计算相关工具方法 ----------------------
|
||||
/**
|
||||
* 示例:Haversine 公式计算两点间距离(单位:米)
|
||||
* @param lat1 第一点纬度
|
||||
* @param lon1 第一点经度
|
||||
* @param lat2 第二点纬度
|
||||
* @param lon2 第二点经度
|
||||
* @return 两点间直线距离(米),计算失败返回 -1
|
||||
*/
|
||||
public static double calculateHaversineDistance(double lat1, double lon1, double lat2, double lon2) {
|
||||
try {
|
||||
final double EARTH_RADIUS = 6371000; // 地球半径(米)
|
||||
// 角度转弧度
|
||||
double latDiff = Math.toRadians(lat2 - lat1);
|
||||
double lonDiff = Math.toRadians(lon2 - lon1);
|
||||
|
||||
// Haversine 核心公式
|
||||
double a = Math.sin(latDiff / 2) * Math.sin(latDiff / 2)
|
||||
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
|
||||
* Math.sin(lonDiff / 2) * Math.sin(lonDiff / 2);
|
||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return EARTH_RADIUS * c; // 返回距离(米)
|
||||
} catch (Exception e) {
|
||||
LogUtils.d(TAG, "Haversine 距离计算失败:" + e.getMessage());
|
||||
return -1; // 标记计算失败
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 计算两点间距离(Haversine公式,纯Java 7 基础API,无数学工具类依赖)
|
||||
* @param gpsLat GPS纬度
|
||||
* @param gpsLon GPS经度
|
||||
* @param posLat 目标位置纬度
|
||||
* @param posLon 目标位置经度
|
||||
* @return 两点间距离(单位:米)
|
||||
*/
|
||||
/*private double calculateHaversineDistance(double gpsLat, double gpsLon, double posLat, double posLon) {
|
||||
final double EARTH_RADIUS = 6371000; // 地球半径(米)
|
||||
double latDiff = Math.toRadians(posLat - gpsLat);
|
||||
double lonDiff = Math.toRadians(posLon - gpsLon);
|
||||
// Haversine公式核心计算(Java 7 基础数学方法)
|
||||
double a = Math.sin(latDiff / 2) * Math.sin(latDiff / 2)
|
||||
+ Math.cos(Math.toRadians(gpsLat)) * Math.cos(Math.toRadians(posLat))
|
||||
* Math.sin(lonDiff / 2) * Math.sin(lonDiff / 2);
|
||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return EARTH_RADIUS * c;
|
||||
}*/
|
||||
|
||||
/**
|
||||
* 校验所有任务触发条件(距离达标则触发任务通知)
|
||||
*/
|
||||
public void checkAllTaskTriggerCondition(PositionModel currentGpsPosition) {
|
||||
if (currentGpsPosition == null) {
|
||||
LogUtils.d(TAG, "传入坐标参数为空,退出函数。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算频率控制模块
|
||||
//
|
||||
// 计算与最近一次GPS计算的时间间隔
|
||||
long nCalculatedTimeBettween = System.currentTimeMillis() - mLastCalculatedTime;
|
||||
// 计算跳跃距离
|
||||
double gpsPositionCalculatedLatitude = mGpsPositionCalculated == null ?0.0f: mGpsPositionCalculated.getLatitude();
|
||||
double gpsPositionCalculatedLongitude = mGpsPositionCalculated == null ?0.0f: mGpsPositionCalculated.getLongitude();
|
||||
double jumpDistance = calculateHaversineDistance(gpsPositionCalculatedLatitude, gpsPositionCalculatedLongitude, currentGpsPosition.getLatitude(), currentGpsPosition.getLongitude());
|
||||
if (jumpDistance < mMinjumpDistance) {
|
||||
LogUtils.d(TAG, String.format("checkAllTaskTriggerCondition:跳跃距离%f,小于50米。", jumpDistance));
|
||||
// 跳跃距离小于最小有效跳跃值
|
||||
if (nCalculatedTimeBettween < mMinCalculatedTimeBettween) {
|
||||
//间隔小于最小时间间隔设定
|
||||
LogUtils.d(TAG, String.format("checkAllTaskTriggerCondition:与最近一次计算间隔时间%d,坐标变化忽略。", nCalculatedTimeBettween));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mGpsPositionCalculated == null) {
|
||||
mGpsPositionCalculated = currentGpsPosition;
|
||||
LogUtils.d(TAG, "最后计算位置记录为空,现在使用新坐标为初始化。");
|
||||
}
|
||||
|
||||
LogUtils.d(TAG, String.format("checkAllTaskTriggerCondition:跳跃距离%f,与上次计算间隔%d,现在启动任务数据计算。", jumpDistance, nCalculatedTimeBettween));
|
||||
|
||||
// 获取位置任务基础数据
|
||||
MainService mainService = MainService.getInstance(mContext);
|
||||
mPositionList = mainService.getPositionList();
|
||||
mAllTasks = mainService.getAllTasks();
|
||||
|
||||
// 位置数据为空,跳过校验。
|
||||
if (mPositionList.isEmpty()) {
|
||||
LogUtils.d(TAG, "checkAllTaskTriggerCondition:位置数据为空,跳过距离计算。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新所有位置点的位置距离数据
|
||||
refreshRealPositionDistance(currentGpsPosition);
|
||||
|
||||
// 任务数据为空,跳过校验。
|
||||
if (mAllTasks.isEmpty()) {
|
||||
LogUtils.d(TAG, "checkAllTaskTriggerCondition:任务数据为空,跳过任务提醒检查计算。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 迭代器遍历任务(Java 7 安全遍历,避免并发修改异常)
|
||||
Iterator<PositionTaskModel> taskIter = mAllTasks.iterator();
|
||||
while (taskIter.hasNext()) {
|
||||
PositionTaskModel task = taskIter.next();
|
||||
// 仅校验“已启用”且“绑定有效位置”的任务
|
||||
if (!task.isEnable() || TextUtils.isEmpty(task.getPositionId())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 查找任务绑定的位置(Java 7 迭代器遍历位置列表)
|
||||
PositionModel bindPos = null;
|
||||
Iterator<PositionModel> posIter = mPositionList.iterator();
|
||||
while (posIter.hasNext()) {
|
||||
PositionModel pos = posIter.next();
|
||||
if (task.getPositionId().equals(pos.getPositionId())) {
|
||||
bindPos = pos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bindPos == null) {
|
||||
LogUtils.w(TAG, "任务ID=" + task.getTaskId() + ":绑定位置不存在,跳过");
|
||||
task.setIsBingo(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 校验任务开始时间
|
||||
if (task.getStartTime() > System.currentTimeMillis()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 校验距离条件(判断是否满足任务触发阈值)
|
||||
double currentDistance = bindPos.getRealPositionDistance();
|
||||
if (currentDistance < 0) {
|
||||
LogUtils.w(TAG, "任务ID=" + task.getTaskId() + ":距离计算失败,跳过");
|
||||
task.setIsBingo(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean isTriggered = false;
|
||||
int taskDistance = task.getDiscussDistance();
|
||||
// 任务触发条件:大于/小于指定距离(Java 7 基础判断,无三元运算符嵌套)
|
||||
if (task.isGreaterThan()) {
|
||||
isTriggered = currentDistance > taskDistance;
|
||||
} else if (task.isLessThan()) {
|
||||
isTriggered = currentDistance < taskDistance;
|
||||
}
|
||||
|
||||
// 更新任务触发状态+发送通知(状态变化时才处理)
|
||||
if (task.isBingo() != isTriggered) {
|
||||
task.setIsBingo(isTriggered);
|
||||
if (isTriggered) {
|
||||
MainService.getInstance(mContext).sendTaskTriggerNotification(task, bindPos, currentDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MainService.getInstance(mContext).saveAllTasks(); // 持久化更新后的任务状态
|
||||
// 记录最后坐标更新点
|
||||
mGpsPositionCalculated = currentGpsPosition;
|
||||
// 记录数据计算时间
|
||||
mLastCalculatedTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 强制刷新所有位置距离(GPS更新后调用,计算距离+校验任务触发条件)
|
||||
*/
|
||||
public void refreshRealPositionDistance(PositionModel currentGpsPosition) {
|
||||
// 遍历所有位置计算距离(Java 7 增强for循环,无Stream)
|
||||
for (PositionModel pos : mPositionList) {
|
||||
if (pos.isEnableRealPositionDistance()) {
|
||||
double distance = DistanceCalculatorUtil.calculateHaversineDistance(
|
||||
currentGpsPosition.getLatitude(),
|
||||
currentGpsPosition.getLongitude(),
|
||||
pos.getLatitude(),
|
||||
pos.getLongitude()
|
||||
);
|
||||
pos.setRealPositionDistance(distance);
|
||||
} else {
|
||||
pos.setRealPositionDistance(-1); // 未启用距离计算,标记为无效
|
||||
}
|
||||
}
|
||||
|
||||
// 距离刷新后通知GPS监听者
|
||||
MainService.getInstance(mContext).notifyAllGpsListeners(currentGpsPosition);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package cc.winboll.studio.positions.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/09/30 16:09
|
||||
* @Describe NotificationUtils(适配API 30,修复系统默认铃声获取,任务通知循环响铃)
|
||||
*/
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.RingtoneManager; // 导入RingtoneManager(关键:用于获取系统默认铃声)
|
||||
import android.net.Uri; // 导入Uri(存储铃声路径)
|
||||
import android.os.Build;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import cc.winboll.studio.positions.R;
|
||||
import cc.winboll.studio.positions.activities.LocationActivity;
|
||||
|
||||
/**
|
||||
* 通知栏工具类:
|
||||
* 1. 任务通知:铃声循环播放(适配API 30),修复系统默认铃声获取方式
|
||||
* 2. 前台服务通知:低打扰(无声无震动),符合API 30规范
|
||||
*/
|
||||
public class NotificationUtil {
|
||||
public static final String TAG = "NotificationUtils";
|
||||
// 任务通知常量(独立渠道,确保循环铃声配置不冲突)
|
||||
private static final String TASK_NOTIFICATION_CHANNEL_ID = "task_notification_channel_01";
|
||||
private static final String TASK_NOTIFICATION_CHANNEL_NAME = "任务通知(循环铃声)";
|
||||
// 前台服务通知常量(独立渠道,低打扰)
|
||||
private static final String FOREGROUND_SERVICE_CHANNEL_ID = "foreground_location_service_channel_02";
|
||||
private static final String FOREGROUND_SERVICE_CHANNEL_NAME = "位置服务";
|
||||
public static final int FOREGROUND_SERVICE_NOTIFICATION_ID = 10086; // 固定前台服务通知ID
|
||||
|
||||
// ---------------------- 核心:任务通知(循环响铃+修复系统默认铃声) ----------------------
|
||||
private static int getNotificationId(String taskId) {
|
||||
return taskId.hashCode() & 0xFFFFFF; // 确保通知ID唯一且非负
|
||||
}
|
||||
|
||||
public static void show(Context context, String taskId, String positionId, String taskDescription) {
|
||||
if (context == null || taskId == null || positionId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (notificationManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 初始化通知渠道(配置循环铃声参数,使用修复后的铃声获取方式)
|
||||
createNotificationChannel(notificationManager);
|
||||
|
||||
// 2. 点击跳转Intent(携带任务/位置参数,适配API 30页面栈)
|
||||
Intent jumpIntent = new Intent(context, LocationActivity.class);
|
||||
jumpIntent.putExtra("EXTRA_POSITION_ID", positionId);
|
||||
jumpIntent.putExtra("EXTRA_TASK_ID", taskId);
|
||||
jumpIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
|
||||
// 3. PendingIntent(API 30强制加IMMUTABLE,避免安全异常)
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
getNotificationId(taskId),
|
||||
jumpIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
||||
);
|
||||
|
||||
// 4. 构建通知(核心:循环响铃+修复的系统默认铃声)
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, TASK_NOTIFICATION_CHANNEL_ID)
|
||||
.setSmallIcon(R.mipmap.ic_launcher) // API 30强制要求,否则通知不显示
|
||||
.setContentTitle("任务提醒")
|
||||
.setContentText(taskDescription)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true) // 点击后取消通知,停止循环响铃
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT) // 确保铃声能正常播放(API 30规则)
|
||||
//.setVibrationPattern(new long[]{0, 300, 200, 300}) // 震动与铃声同步循环
|
||||
.setOnlyAlertOnce(false); // 重复通知也触发循环提醒
|
||||
|
||||
// 关键修复:用RingtoneManager获取系统默认通知铃声(替代废弃的NotificationManager.getDefaultUri)
|
||||
Uri defaultNotificationRingtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
|
||||
if (defaultNotificationRingtone != null) {
|
||||
builder.setSound(defaultNotificationRingtone); // 设置系统默认铃声
|
||||
} else {
|
||||
builder.setDefaults(NotificationCompat.DEFAULT_SOUND); // 极端情况:铃声Uri为空时,用默认提醒音兜底
|
||||
}
|
||||
|
||||
// 5. 循环响铃核心:设置FLAG_INSISTENT(通知未取消则持续循环)
|
||||
Notification notification = builder.build();
|
||||
notification.flags |= Notification.FLAG_INSISTENT;
|
||||
|
||||
// 6. 显示通知(触发循环响铃)
|
||||
notificationManager.notify(getNotificationId(taskId), notification);
|
||||
}
|
||||
|
||||
// ---------------------- 核心:创建通知渠道(修复铃声配置,适配API 30) ----------------------
|
||||
private static void createNotificationChannel(NotificationManager notificationManager) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// 1. 任务通知渠道(用修复后的方式配置系统默认铃声)
|
||||
NotificationChannel taskChannel = new NotificationChannel(
|
||||
TASK_NOTIFICATION_CHANNEL_ID,
|
||||
TASK_NOTIFICATION_CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_DEFAULT // 重要性≥DEFAULT,否则铃声不响(API 30规则)
|
||||
);
|
||||
taskChannel.setDescription("任务提醒通知,铃声循环播放至点击取消");
|
||||
taskChannel.enableVibration(true);
|
||||
taskChannel.setVibrationPattern(new long[]{0, 300, 200, 300});
|
||||
taskChannel.setAllowBubbles(false); // 避免气泡打断循环铃声
|
||||
|
||||
// 关键修复:渠道铃声也用RingtoneManager获取(与通知Builder保持一致,确保铃声统一)
|
||||
Uri channelDefaultRingtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
|
||||
taskChannel.setSound(channelDefaultRingtone, null); // 绑定系统默认铃声到渠道
|
||||
|
||||
// 2. 前台服务渠道(低打扰,无声无震动)
|
||||
NotificationChannel foregroundChannel = new NotificationChannel(
|
||||
FOREGROUND_SERVICE_CHANNEL_ID,
|
||||
FOREGROUND_SERVICE_CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
foregroundChannel.setDescription("位置服务运行中,无声音/震动提醒");
|
||||
foregroundChannel.enableVibration(false);
|
||||
foregroundChannel.setSound(null, null); // 明确关闭铃声
|
||||
foregroundChannel.setShowBadge(false);
|
||||
|
||||
// 注册渠道(API 30覆盖旧配置,确保修复后的铃声生效)
|
||||
notificationManager.createNotificationChannel(taskChannel);
|
||||
notificationManager.createNotificationChannel(foregroundChannel);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- 原有功能:取消任务通知(停止循环响铃) ----------------------
|
||||
public static void cancel(Context context, String taskId) {
|
||||
if (context == null || taskId == null) {
|
||||
return;
|
||||
}
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (notificationManager != null) {
|
||||
notificationManager.cancel(getNotificationId(taskId)); // 取消后循环铃声自动停止
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- 前台服务通知(适配API 30,无声无震动) ----------------------
|
||||
public static Notification createForegroundServiceNotification(Context context, String serviceStatus) {
|
||||
if (context == null) {
|
||||
throw new IllegalArgumentException("Context cannot be null for foreground service notification");
|
||||
}
|
||||
|
||||
// 确保前台服务渠道已创建(低打扰配置)
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (notificationManager != null) {
|
||||
createNotificationChannel(notificationManager);
|
||||
}
|
||||
|
||||
// 点击跳转Intent
|
||||
Intent jumpIntent = new Intent(context, LocationActivity.class);
|
||||
jumpIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
|
||||
// PendingIntent(API 30必加IMMUTABLE)
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
FOREGROUND_SERVICE_NOTIFICATION_ID,
|
||||
jumpIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
|
||||
);
|
||||
|
||||
// 构建前台服务通知(无声无震动,符合低打扰)
|
||||
return new NotificationCompat.Builder(context, FOREGROUND_SERVICE_CHANNEL_ID)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentTitle("位置服务运行中")
|
||||
.setContentText(serviceStatus)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true) // 不可手动清除(前台服务规范)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setDefaults(0) // 禁用所有默认提醒(无声无震动)
|
||||
.build();
|
||||
}
|
||||
|
||||
// ---------------------- 前台服务通知状态更新(适配API 30) ----------------------
|
||||
public static void updateForegroundServiceStatus(Context context, String newServiceStatus) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
Notification updatedNotification = createForegroundServiceNotification(context, newServiceStatus);
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (notificationManager != null) {
|
||||
notificationManager.notify(FOREGROUND_SERVICE_NOTIFICATION_ID, updatedNotification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package cc.winboll.studio.positions.utils;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2024/07/19 14:30:57
|
||||
* @Describe 应用服务组件工具类
|
||||
*/
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.positions.services.AssistantService;
|
||||
import cc.winboll.studio.positions.services.MainService;
|
||||
import java.util.List;
|
||||
|
||||
public class ServiceUtil {
|
||||
public final static String TAG = "ServiceUtil";
|
||||
|
||||
public static boolean isServiceAlive(Context context, String szServiceName) {
|
||||
// 获取Activity管理者对象
|
||||
ActivityManager manager = (ActivityManager) context
|
||||
.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
// 获取正在运行的服务(此处设置最多取1000个)
|
||||
List<ActivityManager.RunningServiceInfo> runningServices = manager
|
||||
.getRunningServices(1000);
|
||||
if (runningServices.size() <= 0) {
|
||||
return false;
|
||||
}
|
||||
// 遍历,若存在名字和传入的serviceName的一致则说明存在
|
||||
for (ActivityManager.RunningServiceInfo runningServiceInfo : runningServices) {
|
||||
if (runningServiceInfo.service.getClassName().equals(szServiceName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void stopAutoService(Context context) {
|
||||
AppConfigsUtil appConfigsUtil = AppConfigsUtil.getInstance(context);
|
||||
appConfigsUtil.setIsEnableMainService(false);
|
||||
appConfigsUtil.saveConfigs();
|
||||
// 关闭并设置主服务
|
||||
Intent intent1 = new Intent(context, MainService.class);
|
||||
intent1.putExtra(MainService.EXTRA_IS_SETTING_TO_ENABLE, false);
|
||||
context.stopService(intent1); // 先停止旧服务
|
||||
context.startService(intent1); // 传入新的启动标志位,返回给系统
|
||||
// 关闭并设置主服务守护进程
|
||||
Intent intent2 = new Intent(context, AssistantService.class);
|
||||
intent2.putExtra(AssistantService.EXTRA_IS_SETTING_TO_ENABLE, false);
|
||||
context.stopService(intent2); // 先停止旧服务
|
||||
context.startService(intent2); // 传入新的启动标志位,返回给系统
|
||||
// 再次关闭所有服务
|
||||
context.stopService(intent1);
|
||||
context.stopService(intent2);
|
||||
|
||||
LogUtils.d(TAG, "stopAutoService");
|
||||
}
|
||||
|
||||
public static void startAutoService(Context context) {
|
||||
AppConfigsUtil appConfigsUtil = AppConfigsUtil.getInstance(context);
|
||||
appConfigsUtil.setIsEnableMainService(true);
|
||||
appConfigsUtil.saveConfigs();
|
||||
Intent intent = new Intent(context, MainService.class);
|
||||
intent.putExtra(MainService.EXTRA_IS_SETTING_TO_ENABLE, true);
|
||||
context.startService(intent);
|
||||
LogUtils.d(TAG, "startAutoService");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package cc.winboll.studio.positions.views;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/10/22 02:15
|
||||
* @Describe DateTimePickerPopup
|
||||
*/
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.PopupWindow;
|
||||
import java.util.Calendar;
|
||||
import cc.winboll.studio.positions.R;
|
||||
import cc.winboll.studio.positions.utils.DensityUtils;
|
||||
|
||||
/**
|
||||
* 日期时间选择弹窗(竖直滚动行:年、月、日、时、分)
|
||||
*/
|
||||
public class DateTimePickerPopup extends PopupWindow {
|
||||
public static final String TAG = "DateTimePickerPopup";
|
||||
|
||||
private Context mContext;
|
||||
private NumberPicker mPickerYear;
|
||||
private NumberPicker mPickerMonth;
|
||||
private NumberPicker mPickerDay;
|
||||
private NumberPicker mPickerHour;
|
||||
private NumberPicker mPickerMinute;
|
||||
private Button mBtnCancel;
|
||||
private Button mBtnConfirm;
|
||||
private OnDateTimeSelectedListener mListener;
|
||||
|
||||
// 时间范围默认值
|
||||
private int mMinYear = 2000;
|
||||
private int mMaxYear = Calendar.getInstance().get(Calendar.YEAR) + 10;
|
||||
private int mMinMonth = 1;
|
||||
private int mMaxMonth = 12;
|
||||
private int mMinDay = 1;
|
||||
private int mMaxDay = 31;
|
||||
private int mMinHour = 0;
|
||||
private int mMaxHour = 23;
|
||||
private int mMinMinute = 0;
|
||||
private int mMaxMinute = 59;
|
||||
|
||||
// 默认选中时间
|
||||
private int mDefaultYear;
|
||||
private int mDefaultMonth;
|
||||
private int mDefaultDay;
|
||||
private int mDefaultHour;
|
||||
private int mDefaultMinute;
|
||||
|
||||
/**
|
||||
* 日期时间选择回调
|
||||
*/
|
||||
public interface OnDateTimeSelectedListener {
|
||||
void onDateTimeSelected(int year, int month, int day, int hour, int minute);
|
||||
void onCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder 模式
|
||||
*/
|
||||
public static class Builder {
|
||||
private Context mContext;
|
||||
private DateTimePickerPopup mPopup;
|
||||
|
||||
public Builder(Context context) {
|
||||
this.mContext = context;
|
||||
mPopup = new DateTimePickerPopup(context);
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
mPopup.mDefaultYear = calendar.get(Calendar.YEAR);
|
||||
mPopup.mDefaultMonth = calendar.get(Calendar.MONTH) + 1;
|
||||
mPopup.mDefaultDay = calendar.get(Calendar.DAY_OF_MONTH);
|
||||
mPopup.mDefaultHour = calendar.get(Calendar.HOUR_OF_DAY);
|
||||
mPopup.mDefaultMinute = calendar.get(Calendar.MINUTE);
|
||||
}
|
||||
|
||||
public Builder setDateTimeRange(int minYear, int maxYear, int minMonth, int maxMonth,
|
||||
int minDay, int maxDay, int minHour, int maxHour,
|
||||
int minMinute, int maxMinute) {
|
||||
mPopup.mMinYear = minYear;
|
||||
mPopup.mMaxYear = maxYear;
|
||||
mPopup.mMinMonth = minMonth;
|
||||
mPopup.mMaxMonth = maxMonth;
|
||||
mPopup.mMinDay = minDay;
|
||||
mPopup.mMaxDay = maxDay;
|
||||
mPopup.mMinHour = minHour;
|
||||
mPopup.mMaxHour = maxHour;
|
||||
mPopup.mMinMinute = minMinute;
|
||||
mPopup.mMaxMinute = maxMinute;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setDefaultDateTime(int year, int month, int day, int hour, int minute) {
|
||||
mPopup.mDefaultYear = year;
|
||||
mPopup.mDefaultMonth = month;
|
||||
mPopup.mDefaultDay = day;
|
||||
mPopup.mDefaultHour = hour;
|
||||
mPopup.mDefaultMinute = minute;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setOnDateTimeSelectedListener(OnDateTimeSelectedListener listener) {
|
||||
mPopup.mListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DateTimePickerPopup build() {
|
||||
mPopup.initView();
|
||||
mPopup.initPickers();
|
||||
mPopup.bindButtonClick();
|
||||
mPopup.setPopupStyle();
|
||||
return mPopup;
|
||||
}
|
||||
}
|
||||
|
||||
private DateTimePickerPopup(Context context) {
|
||||
super(context);
|
||||
this.mContext = context;
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
LayoutInflater inflater = LayoutInflater.from(mContext);
|
||||
View rootView = inflater.inflate(R.layout.dialog_date_time_picker, null, false);
|
||||
setContentView(rootView);
|
||||
|
||||
mPickerYear = (NumberPicker) rootView.findViewById(R.id.picker_year);
|
||||
mPickerMonth = (NumberPicker) rootView.findViewById(R.id.picker_month);
|
||||
mPickerDay = (NumberPicker) rootView.findViewById(R.id.picker_day);
|
||||
mPickerHour = (NumberPicker) rootView.findViewById(R.id.picker_hour);
|
||||
mPickerMinute = (NumberPicker) rootView.findViewById(R.id.picker_minute);
|
||||
mBtnCancel = (Button) rootView.findViewById(R.id.btn_cancel);
|
||||
mBtnConfirm = (Button) rootView.findViewById(R.id.btn_confirm);
|
||||
}
|
||||
|
||||
private void initPickers() {
|
||||
// 初始化年选择器
|
||||
mPickerYear.setMinValue(mMinYear);
|
||||
mPickerYear.setMaxValue(mMaxYear);
|
||||
mPickerYear.setValue(mDefaultYear);
|
||||
mPickerYear.setWrapSelectorWheel(false);
|
||||
|
||||
// 初始化月选择器
|
||||
mPickerMonth.setMinValue(mMinMonth);
|
||||
mPickerMonth.setMaxValue(mMaxMonth);
|
||||
mPickerMonth.setValue(mDefaultMonth);
|
||||
mPickerMonth.setWrapSelectorWheel(false);
|
||||
|
||||
// 初始化日选择器(根据年月动态调整范围)
|
||||
updateDayRange(mDefaultYear, mDefaultMonth);
|
||||
mPickerDay.setValue(mDefaultDay);
|
||||
mPickerDay.setWrapSelectorWheel(false);
|
||||
|
||||
// 初始化时选择器
|
||||
mPickerHour.setMinValue(mMinHour);
|
||||
mPickerHour.setMaxValue(mMaxHour);
|
||||
mPickerHour.setValue(mDefaultHour);
|
||||
mPickerHour.setWrapSelectorWheel(false);
|
||||
|
||||
// 初始化分选择器
|
||||
mPickerMinute.setMinValue(mMinMinute);
|
||||
mPickerMinute.setMaxValue(mMaxMinute);
|
||||
mPickerMinute.setValue(mDefaultMinute);
|
||||
mPickerMinute.setWrapSelectorWheel(false);
|
||||
|
||||
// 年月变化时更新日范围(Java 7 匿名内部类)
|
||||
mPickerYear.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
|
||||
@Override
|
||||
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
|
||||
updateDayRange(newVal, mPickerMonth.getValue());
|
||||
}
|
||||
});
|
||||
|
||||
mPickerMonth.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
|
||||
@Override
|
||||
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
|
||||
updateDayRange(mPickerYear.getValue(), newVal);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateDayRange(int year, int month) {
|
||||
int maxDay;
|
||||
switch (month) {
|
||||
case 2:
|
||||
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
|
||||
maxDay = 29;
|
||||
} else {
|
||||
maxDay = 28;
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
case 6:
|
||||
case 9:
|
||||
case 11:
|
||||
maxDay = 30;
|
||||
break;
|
||||
default:
|
||||
maxDay = 31;
|
||||
}
|
||||
mPickerDay.setMaxValue(maxDay);
|
||||
if (mPickerDay.getValue() > maxDay) {
|
||||
mPickerDay.setValue(maxDay);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindButtonClick() {
|
||||
// 取消按钮(Java 7 匿名内部类)
|
||||
mBtnCancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
dismiss();
|
||||
if (mListener != null) {
|
||||
mListener.onCancel();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 确认按钮(Java 7 匿名内部类)
|
||||
mBtnConfirm.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int year = mPickerYear.getValue();
|
||||
int month = mPickerMonth.getValue();
|
||||
int day = mPickerDay.getValue();
|
||||
int hour = mPickerHour.getValue();
|
||||
int minute = mPickerMinute.getValue();
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onDateTimeSelected(year, month, day, hour, minute);
|
||||
}
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setPopupStyle() {
|
||||
int width = (int) (DensityUtils.getScreenWidth(mContext) * 0.85f);
|
||||
int height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
|
||||
setWidth(width);
|
||||
setHeight(height);
|
||||
setFocusable(true);
|
||||
setOutsideTouchable(true);
|
||||
setBackgroundDrawable(mContext.getResources().getDrawable(R.drawable.bg_dialog_round));
|
||||
setAnimationStyle(R.style.PopupDateTimePickerAnim);
|
||||
}
|
||||
|
||||
public void showAsDropDown(View anchorView) {
|
||||
super.showAsDropDown(anchorView, 0, DensityUtils.dp2px(mContext, 10));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
package cc.winboll.studio.positions.views;
|
||||
|
||||
/**
|
||||
* @Describe 位置任务列表视图(适配MainService唯一数据源+同步任务状态+支持简单/编辑模式)
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @CreateTime 2025-09-30 08:09:00
|
||||
* @EditTime 2026-03-31 19:00:00
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import cc.winboll.studio.libaes.dialogs.YesNoAlertDialog;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
import cc.winboll.studio.positions.R;
|
||||
import cc.winboll.studio.positions.models.PositionTaskModel;
|
||||
import cc.winboll.studio.positions.services.MainService;
|
||||
import com.jzxiang.pickerview.TimePickerDialog;
|
||||
import com.jzxiang.pickerview.listener.OnDateSetListener;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class PositionTaskListView extends LinearLayout {
|
||||
|
||||
// 视图模式常量
|
||||
public static final int VIEW_MODE_SIMPLE = 1;
|
||||
public static final int VIEW_MODE_EDIT = 2;
|
||||
|
||||
private static final String TAG = "PositionTaskListView";
|
||||
|
||||
// 核心成员变量
|
||||
private String mBindPositionId;
|
||||
private MainService mMainService;
|
||||
private int mCurrentViewMode;
|
||||
private TaskListAdapter mTaskAdapter;
|
||||
private RecyclerView mRvTasks;
|
||||
|
||||
// 任务修改回调接口
|
||||
public interface OnTaskUpdatedListener {
|
||||
void onTaskUpdated(String positionId, ArrayList<PositionTaskModel> updatedTasks);
|
||||
}
|
||||
|
||||
private OnTaskUpdatedListener mOnTaskUpdatedListener;
|
||||
|
||||
// ---------------------- 构造函数 ----------------------
|
||||
public PositionTaskListView(Context context) {
|
||||
super(context);
|
||||
LogUtils.d(TAG, "PositionTaskListView 构造函数 1 参数: context=" + context);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public PositionTaskListView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
LogUtils.d(TAG, "PositionTaskListView 构造函数 2 参数: context=" + context + ", attrs=" + attrs);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
public PositionTaskListView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
LogUtils.d(TAG, "PositionTaskListView 构造函数 3 参数: context=" + context + ", attrs=" + attrs + ", defStyleAttr=" + defStyleAttr);
|
||||
initView(context);
|
||||
}
|
||||
|
||||
// ---------------------- 初始化视图 ----------------------
|
||||
private void initView(Context context) {
|
||||
LogUtils.d(TAG, "initView 开始执行 参数: context=" + context);
|
||||
setOrientation(VERTICAL);
|
||||
LayoutInflater.from(context).inflate(R.layout.view_position_task_list, this, true);
|
||||
|
||||
mRvTasks = (RecyclerView) findViewById(R.id.rv_position_tasks);
|
||||
mRvTasks.setLayoutManager(new LinearLayoutManager(context));
|
||||
|
||||
mTaskAdapter = new TaskListAdapter(new ArrayList<PositionTaskModel>());
|
||||
mRvTasks.setAdapter(mTaskAdapter);
|
||||
|
||||
mCurrentViewMode = VIEW_MODE_SIMPLE;
|
||||
LogUtils.d(TAG, "initView 执行完成,视图初始化完毕");
|
||||
}
|
||||
|
||||
// ---------------------- 对外API ----------------------
|
||||
public void init(MainService mainService, String positionId) {
|
||||
LogUtils.d(TAG, "init 开始执行 参数: mainService=" + mainService + ", positionId=" + positionId);
|
||||
if (mainService == null) {
|
||||
LogUtils.e(TAG, "init 失败:MainService 为空");
|
||||
showToast("任务列表初始化失败:服务未就绪");
|
||||
return;
|
||||
}
|
||||
if (positionId == null || positionId.trim().isEmpty()) {
|
||||
LogUtils.e(TAG, "init 失败:positionId 为空");
|
||||
showToast("任务列表初始化失败:未关联位置");
|
||||
return;
|
||||
}
|
||||
|
||||
mMainService = mainService;
|
||||
mBindPositionId = positionId;
|
||||
LogUtils.d(TAG, "init 绑定成功:positionId=" + positionId);
|
||||
|
||||
syncTasksFromMainService();
|
||||
LogUtils.d(TAG, "init 执行完成");
|
||||
}
|
||||
|
||||
public void syncTasksFromMainService() {
|
||||
LogUtils.d(TAG, "syncTasksFromMainService 开始执行");
|
||||
if (mMainService == null || mBindPositionId == null || mBindPositionId.trim().isEmpty()) {
|
||||
LogUtils.w(TAG, "syncTasksFromMainService 失败:服务或位置ID无效");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ArrayList<PositionTaskModel> allServiceTasks = mMainService.getAllTasks();
|
||||
LogUtils.d(TAG, "syncTasksFromMainService 从服务获取任务总数: " + (allServiceTasks == null ? 0 : allServiceTasks.size()));
|
||||
|
||||
ArrayList<PositionTaskModel> currentPosTasks = new ArrayList<>();
|
||||
if (allServiceTasks != null && !allServiceTasks.isEmpty()) {
|
||||
for (PositionTaskModel task : allServiceTasks) {
|
||||
if (isTaskMatchedWithPosition(task)) {
|
||||
currentPosTasks.add(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
LogUtils.d(TAG, "syncTasksFromMainService 筛选后当前位置任务数: " + currentPosTasks.size());
|
||||
|
||||
mTaskAdapter.updateData(currentPosTasks);
|
||||
LogUtils.d(TAG, "syncTasksFromMainService 执行完成,Adapter已刷新");
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "syncTasksFromMainService 异常: " + e.getMessage(), e);
|
||||
showToast("任务同步失败,请重试");
|
||||
}
|
||||
}
|
||||
|
||||
public void setViewStatus(int viewMode) {
|
||||
LogUtils.d(TAG, "setViewStatus 参数: viewMode=" + viewMode);
|
||||
if (viewMode != VIEW_MODE_SIMPLE && viewMode != VIEW_MODE_EDIT) {
|
||||
LogUtils.w(TAG, "setViewStatus 无效模式");
|
||||
return;
|
||||
}
|
||||
mCurrentViewMode = viewMode;
|
||||
mTaskAdapter.notifyDataSetChanged();
|
||||
LogUtils.d(TAG, "setViewStatus 切换完成: " + (viewMode == VIEW_MODE_SIMPLE ? "简单模式" : "编辑模式"));
|
||||
}
|
||||
|
||||
public void setOnTaskUpdatedListener(OnTaskUpdatedListener listener) {
|
||||
LogUtils.d(TAG, "setOnTaskUpdatedListener 参数: listener=" + listener);
|
||||
mOnTaskUpdatedListener = listener;
|
||||
}
|
||||
|
||||
public ArrayList<PositionTaskModel> getCurrentPosTasks() {
|
||||
LogUtils.d(TAG, "getCurrentPosTasks 被调用");
|
||||
return new ArrayList<>(mTaskAdapter.getAdapterData());
|
||||
}
|
||||
|
||||
public void clearData() {
|
||||
LogUtils.d(TAG, "clearData 开始执行");
|
||||
mTaskAdapter.updateData(new ArrayList<PositionTaskModel>());
|
||||
mMainService = null;
|
||||
mBindPositionId = null;
|
||||
mCurrentViewMode = VIEW_MODE_SIMPLE;
|
||||
LogUtils.d(TAG, "clearData 执行完成,数据已清空");
|
||||
}
|
||||
|
||||
public void triggerTaskSync() {
|
||||
LogUtils.d(TAG, "triggerTaskSync 主动触发同步");
|
||||
syncTasksFromMainService();
|
||||
if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
|
||||
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, getCurrentPosTasks());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- 内部工具 ----------------------
|
||||
private boolean isTaskMatchedWithPosition(PositionTaskModel task) {
|
||||
if (task == null || mBindPositionId == null) {
|
||||
return false;
|
||||
}
|
||||
return mBindPositionId.equals(task.getPositionId());
|
||||
}
|
||||
|
||||
private void showToast(String content) {
|
||||
if (getContext() == null) return;
|
||||
Toast.makeText(getContext(), content, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private String genSelectedTimeText(long timeMillis) {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
|
||||
return sdf.format(new Date(timeMillis));
|
||||
}
|
||||
|
||||
// ---------------------- 外部新增任务 ----------------------
|
||||
public void addNewTask(PositionTaskModel newTask) {
|
||||
LogUtils.d(TAG, "addNewTask 参数: newTask=" + newTask);
|
||||
if (mMainService == null) {
|
||||
LogUtils.e(TAG, "addNewTask 失败:MainService为空");
|
||||
showToast("新增任务失败:服务未就绪");
|
||||
return;
|
||||
}
|
||||
if (newTask == null) {
|
||||
LogUtils.e(TAG, "addNewTask 失败:任务为空");
|
||||
showToast("新增任务失败:任务数据为空");
|
||||
return;
|
||||
}
|
||||
if (mBindPositionId == null || mBindPositionId.trim().isEmpty()) {
|
||||
LogUtils.e(TAG, "addNewTask 失败:未绑定位置");
|
||||
showToast("新增任务失败:未关联位置");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
newTask.setPositionId(mBindPositionId);
|
||||
mMainService.addTask(newTask);
|
||||
LogUtils.d(TAG, "addNewTask 服务添加成功 taskId=" + newTask.getTaskId());
|
||||
|
||||
syncTasksFromMainService();
|
||||
if (mOnTaskUpdatedListener != null) {
|
||||
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, getCurrentPosTasks());
|
||||
}
|
||||
showToast("新增任务成功");
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "addNewTask 异常: " + e.getMessage(), e);
|
||||
showToast("新增失败,请重试");
|
||||
syncTasksFromMainService();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- Adapter ----------------------
|
||||
private class TaskListAdapter extends RecyclerView.Adapter<TaskListAdapter.TaskViewHolder> {
|
||||
|
||||
private List<PositionTaskModel> mAdapterData;
|
||||
|
||||
public TaskListAdapter(List<PositionTaskModel> data) {
|
||||
LogUtils.d(TAG, "TaskListAdapter 构造 参数 data.size=" + data.size());
|
||||
mAdapterData = new ArrayList<>(data);
|
||||
}
|
||||
|
||||
public void updateData(List<PositionTaskModel> newData) {
|
||||
LogUtils.d(TAG, "updateData 参数 newData.size=" + (newData == null ? 0 : newData.size()));
|
||||
if (newData == null) {
|
||||
mAdapterData.clear();
|
||||
} else {
|
||||
mAdapterData = new ArrayList<>(newData);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public List<PositionTaskModel> getAdapterData() {
|
||||
return new ArrayList<>(mAdapterData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mAdapterData.isEmpty() ? 1 : mAdapterData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (mAdapterData.isEmpty()) {
|
||||
return 0;
|
||||
} else {
|
||||
return mCurrentViewMode;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public TaskViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
Context context = parent.getContext();
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
|
||||
if (viewType == 0) {
|
||||
View emptyView = inflater.inflate(R.layout.item_task_empty, parent, false);
|
||||
return new EmptyViewHolder(emptyView);
|
||||
} else if (viewType == VIEW_MODE_SIMPLE) {
|
||||
View simpleView = inflater.inflate(R.layout.item_position_task_simple, parent, false);
|
||||
return new SimpleTaskViewHolder(simpleView);
|
||||
} else {
|
||||
View editView = inflater.inflate(R.layout.item_task_content, parent, false);
|
||||
return new TaskContentViewHolder(editView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull TaskViewHolder holder, int position) {
|
||||
if (holder instanceof EmptyViewHolder) {
|
||||
EmptyViewHolder emptyHolder = (EmptyViewHolder) holder;
|
||||
TextView tv = emptyHolder.itemView.findViewById(R.id.tv_task_empty_tip);
|
||||
tv.setText(mCurrentViewMode == VIEW_MODE_EDIT
|
||||
? "暂无任务,点击\"添加新任务\"创建"
|
||||
: "暂无启用的任务");
|
||||
return;
|
||||
}
|
||||
|
||||
if (position >= mAdapterData.size()) {
|
||||
LogUtils.w(TAG, "onBindViewHolder 越界 position=" + position);
|
||||
return;
|
||||
}
|
||||
|
||||
final PositionTaskModel task = mAdapterData.get(position);
|
||||
if (task == null) {
|
||||
LogUtils.w(TAG, "onBindViewHolder task为空 position=" + position);
|
||||
return;
|
||||
}
|
||||
|
||||
if (holder instanceof SimpleTaskViewHolder) {
|
||||
SimpleTaskViewHolder h = (SimpleTaskViewHolder) holder;
|
||||
String desc = task.getTaskDescription() == null ? "未设置描述" : task.getTaskDescription();
|
||||
h.tvSimpleTaskDesc.setText(String.format("任务:%s", desc));
|
||||
h.tvStartTime.setText(genSelectedTimeText(task.getStartTime()));
|
||||
String cond = task.isGreaterThan() ? "大于(◎)" : "小于(•)";
|
||||
h.tvSimpleDistanceCond.setText(String.format("条件:距离 %s %d 米", cond, task.getDiscussDistance()));
|
||||
h.tvSimpleIsEnable.setText(task.isEnable() ? "状态:已启用" : "状态:已禁用");
|
||||
h.tvSimpleIsEnable.setTextColor(task.isEnable()
|
||||
? getContext().getResources().getColor(R.color.colorEnableGreen)
|
||||
: getContext().getResources().getColor(R.color.colorGrayText));
|
||||
h.vBingoDot.setVisibility(task.isBingo() ? View.VISIBLE : View.GONE);
|
||||
|
||||
} else if (holder instanceof TaskContentViewHolder) {
|
||||
TaskContentViewHolder h = (TaskContentViewHolder) holder;
|
||||
bindEditModeTask(h, task, position);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindEditModeTask(final TaskContentViewHolder holder, final PositionTaskModel task, final int position) {
|
||||
LogUtils.d(TAG, "bindEditModeTask position=" + position + " taskId=" + task.getTaskId());
|
||||
|
||||
String desc = task.getTaskDescription() == null ? "未设置描述" : task.getTaskDescription();
|
||||
holder.tvTaskDesc.setText(String.format("任务:%s", desc));
|
||||
String cond = task.isGreaterThan() ? "大于(◎)" : "小于(•)";
|
||||
holder.tvTaskDistance.setText(String.format("条件:%s %d 米", cond, task.getDiscussDistance()));
|
||||
holder.tvStartTime.setText(genSelectedTimeText(task.getStartTime()));
|
||||
|
||||
holder.cbTaskEnable.setOnCheckedChangeListener(null);
|
||||
holder.cbTaskEnable.setChecked(task.isEnable());
|
||||
holder.cbTaskEnable.setEnabled(mCurrentViewMode == VIEW_MODE_EDIT);
|
||||
|
||||
holder.btnEditTask.setVisibility(View.VISIBLE);
|
||||
holder.btnDeleteTask.setVisibility(View.VISIBLE);
|
||||
|
||||
holder.btnDeleteTask.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
YesNoAlertDialog.show(getContext(), "删除任务提示", "是否删除此项位置任务?", new YesNoAlertDialog.OnDialogResultListener(){
|
||||
@Override
|
||||
public void onNo() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onYes() {
|
||||
LogUtils.d(TAG, "删除按钮点击 position=" + position + " taskId=" + task.getTaskId());
|
||||
if (mMainService == null) {
|
||||
showToast("删除失败:服务未就绪");
|
||||
LogUtils.e(TAG, "删除失败:MainService为空");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mMainService.deleteTask(task.getTaskId());
|
||||
LogUtils.d(TAG, "服务删除任务成功 taskId=" + task.getTaskId());
|
||||
notifyItemRemoved(position);
|
||||
notifyItemRangeChanged(position, mAdapterData.size());
|
||||
|
||||
if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
|
||||
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList<>(mAdapterData));
|
||||
}
|
||||
showToast("任务已删除");
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "删除异常: " + e.getMessage(), e);
|
||||
showToast("删除失败,请重试");
|
||||
syncTasksFromMainService();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
holder.btnEditTask.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LogUtils.d(TAG, "编辑按钮点击 position=" + position + " taskId=" + task.getTaskId());
|
||||
if (mMainService == null) {
|
||||
showToast("编辑失败:服务未就绪");
|
||||
LogUtils.e(TAG, "编辑失败:MainService为空");
|
||||
return;
|
||||
}
|
||||
showEditTaskDialog(task, position);
|
||||
}
|
||||
});
|
||||
|
||||
holder.cbTaskEnable.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, final boolean isChecked) {
|
||||
LogUtils.d(TAG, "开关状态变更 taskId=" + task.getTaskId() + " isChecked=" + isChecked);
|
||||
if (mMainService == null) {
|
||||
showToast("状态修改失败:服务未就绪");
|
||||
buttonView.setChecked(!isChecked);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
task.setIsEnable(isChecked);
|
||||
mMainService.updateTaskStatus(task);
|
||||
mRvTasks.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notifyItemChanged(position);
|
||||
if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
|
||||
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList<>(mAdapterData));
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "开关变更异常: " + e.getMessage(), e);
|
||||
buttonView.setChecked(!isChecked);
|
||||
task.setIsEnable(!isChecked);
|
||||
syncTasksFromMainService();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showEditTaskDialog(final PositionTaskModel task, final int position) {
|
||||
LogUtils.d(TAG, "showEditTaskDialog position=" + position + " taskId=" + task.getTaskId());
|
||||
final Context context = getContext();
|
||||
if (context == null) {
|
||||
LogUtils.w(TAG, "showEditTaskDialog 上下文为空");
|
||||
return;
|
||||
}
|
||||
|
||||
View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_edit_task, null);
|
||||
final EditText etEditDesc = dialogView.findViewById(R.id.et_edit_task_desc);
|
||||
final RadioGroup rgDistanceCondition = dialogView.findViewById(R.id.rg_distance_condition);
|
||||
final EditText etEditDistance = dialogView.findViewById(R.id.et_edit_distance);
|
||||
Button btnCancel = dialogView.findViewById(R.id.btn_dialog_cancel);
|
||||
Button btnSave = dialogView.findViewById(R.id.btn_dialog_save);
|
||||
final Button btnSelectTime = dialogView.findViewById(R.id.btn_select_time);
|
||||
final TextView tvSelectedTime = dialogView.findViewById(R.id.tv_selected_time);
|
||||
|
||||
tvSelectedTime.setText(genSelectedTimeText(task.getStartTime()));
|
||||
|
||||
Calendar initCal = Calendar.getInstance();
|
||||
initCal.setTimeInMillis(task.getStartTime());
|
||||
int y = initCal.get(Calendar.YEAR);
|
||||
int M = initCal.get(Calendar.MONTH) + 1;
|
||||
int d = initCal.get(Calendar.DAY_OF_MONTH);
|
||||
int h = initCal.get(Calendar.HOUR_OF_DAY);
|
||||
int m = initCal.get(Calendar.MINUTE);
|
||||
|
||||
final DateTimePickerPopup dateTimePopup = new DateTimePickerPopup.Builder(context)
|
||||
.setDateTimeRange(2020, 2030, 1, 12, 1, 31, 0, 23, 0, 59)
|
||||
.setDefaultDateTime(y, M, d, h, m)
|
||||
.setOnDateTimeSelectedListener(new DateTimePickerPopup.OnDateTimeSelectedListener() {
|
||||
@Override
|
||||
public void onDateTimeSelected(int year, int month, int day, int hour, int minute) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.set(year, month - 1, day, hour, minute, 0);
|
||||
cal.set(Calendar.MILLISECOND, 0);
|
||||
long time = cal.getTimeInMillis();
|
||||
tvSelectedTime.setText(genSelectedTimeText(time));
|
||||
task.setStartTime(time);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {}
|
||||
})
|
||||
.build();
|
||||
|
||||
btnSelectTime.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
dateTimePopup.showAsDropDown(btnSelectTime);
|
||||
}
|
||||
});
|
||||
|
||||
etEditDesc.setText(task.getTaskDescription() == null ? "" : task.getTaskDescription());
|
||||
etEditDesc.setSelection(etEditDesc.getText().length());
|
||||
|
||||
if (task.isGreaterThan()) {
|
||||
rgDistanceCondition.check(R.id.rb_greater_than);
|
||||
} else {
|
||||
rgDistanceCondition.check(R.id.rb_less_than);
|
||||
}
|
||||
etEditDistance.setText(String.valueOf(task.getDiscussDistance()));
|
||||
|
||||
final android.app.AlertDialog dialog = new android.app.AlertDialog.Builder(context)
|
||||
.setView(dialogView)
|
||||
.setCancelable(false)
|
||||
.create();
|
||||
dialog.show();
|
||||
|
||||
final OnDateSetListener listener = new OnDateSetListener() {
|
||||
@Override
|
||||
public void onDateSet(TimePickerDialog timePickerDialog, long p) {}
|
||||
};
|
||||
|
||||
btnCancel.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
btnSave.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String newDesc = etEditDesc.getText().toString().trim();
|
||||
String distStr = etEditDistance.getText().toString().trim();
|
||||
|
||||
if (distStr.isEmpty()) {
|
||||
showToast("请输入有效距离");
|
||||
etEditDistance.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
int newDist;
|
||||
try {
|
||||
newDist = Integer.parseInt(distStr);
|
||||
if (newDist < 1) {
|
||||
showToast("距离不能小于1米");
|
||||
etEditDistance.requestFocus();
|
||||
return;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
showToast("距离请输入数字");
|
||||
etEditDistance.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
task.setTaskDescription(newDesc);
|
||||
task.setDiscussDistance(newDist);
|
||||
boolean isGreater = rgDistanceCondition.getCheckedRadioButtonId() == R.id.rb_greater_than;
|
||||
task.setIsGreaterThan(isGreater);
|
||||
task.setPositionId(mBindPositionId);
|
||||
|
||||
try {
|
||||
mMainService.updateTask(task);
|
||||
LogUtils.d(TAG, "保存修改成功 taskId=" + task.getTaskId());
|
||||
mAdapterData.set(position, task);
|
||||
mRvTasks.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notifyItemChanged(position);
|
||||
if (mOnTaskUpdatedListener != null && mBindPositionId != null) {
|
||||
mOnTaskUpdatedListener.onTaskUpdated(mBindPositionId, new ArrayList<>(mAdapterData));
|
||||
}
|
||||
}
|
||||
});
|
||||
dialog.dismiss();
|
||||
showToast("任务已更新");
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "保存异常: " + e.getMessage(), e);
|
||||
showToast("保存失败,请重试");
|
||||
syncTasksFromMainService();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------- ViewHolder ----------------------
|
||||
public abstract class TaskViewHolder extends RecyclerView.ViewHolder {
|
||||
public TaskViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
|
||||
public class EmptyViewHolder extends TaskViewHolder {
|
||||
public EmptyViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
|
||||
public class SimpleTaskViewHolder extends TaskViewHolder {
|
||||
TextView tvSimpleTaskDesc;
|
||||
TextView tvSimpleDistanceCond;
|
||||
TextView tvStartTime;
|
||||
TextView tvSimpleIsEnable;
|
||||
View vBingoDot;
|
||||
|
||||
public SimpleTaskViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvSimpleTaskDesc = itemView.findViewById(R.id.tv_simple_task_desc);
|
||||
tvSimpleDistanceCond = itemView.findViewById(R.id.tv_simple_distance_cond);
|
||||
tvStartTime = itemView.findViewById(R.id.tv_starttime);
|
||||
tvSimpleIsEnable = itemView.findViewById(R.id.tv_simple_is_enable);
|
||||
vBingoDot = itemView.findViewById(R.id.v_bingo_dot);
|
||||
}
|
||||
}
|
||||
|
||||
public class TaskContentViewHolder extends TaskViewHolder {
|
||||
TextView tvTaskDesc;
|
||||
TextView tvTaskDistance;
|
||||
TextView tvStartTime;
|
||||
CompoundButton cbTaskEnable;
|
||||
Button btnEditTask;
|
||||
Button btnDeleteTask;
|
||||
|
||||
public TaskContentViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvTaskDesc = itemView.findViewById(R.id.tv_task_desc);
|
||||
tvTaskDistance = itemView.findViewById(R.id.tv_task_distance);
|
||||
tvStartTime = itemView.findViewById(R.id.tv_starttime);
|
||||
cbTaskEnable = itemView.findViewById(R.id.cb_task_enable);
|
||||
btnEditTask = itemView.findViewById(R.id.btn_edit_task);
|
||||
btnDeleteTask = itemView.findViewById(R.id.btn_delete_task);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
positions/src/main/res/anim/popup_date_time_picker_in.xml
Normal file
12
positions/src/main/res/anim/popup_date_time_picker_in.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="300">
|
||||
<translate
|
||||
android:fromYDelta="50%"
|
||||
android:toYDelta="0%"
|
||||
android:interpolator="@android:anim/decelerate_interpolator" />
|
||||
<alpha
|
||||
android:fromAlpha="0.6"
|
||||
android:toAlpha="1.0"
|
||||
android:interpolator="@android:anim/decelerate_interpolator" />
|
||||
</set>
|
||||
12
positions/src/main/res/anim/popup_date_time_picker_out.xml
Normal file
12
positions/src/main/res/anim/popup_date_time_picker_out.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="250">
|
||||
<translate
|
||||
android:fromYDelta="0%"
|
||||
android:toYDelta="50%"
|
||||
android:interpolator="@android:anim/accelerate_interpolator" />
|
||||
<alpha
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.6"
|
||||
android:interpolator="@android:anim/accelerate_interpolator" />
|
||||
</set>
|
||||
BIN
positions/src/main/res/drawable/activity_background.png
Normal file
BIN
positions/src/main/res/drawable/activity_background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
8
positions/src/main/res/drawable/bg_bingo_dot.xml
Normal file
8
positions/src/main/res/drawable/bg_bingo_dot.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval"> <!-- 形状:圆形 -->
|
||||
<solid android:color="#FF4444"/> <!-- 填充色:亮红色(可调整色值) -->
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#FFFFFF"/>
|
||||
</shape>
|
||||
13
positions/src/main/res/drawable/bg_dialog_round.xml
Normal file
13
positions/src/main/res/drawable/bg_dialog_round.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<!-- 白色背景 -->
|
||||
<solid android:color="#FFFFFF" />
|
||||
<!-- 圆角(12dp,可根据需求调整) -->
|
||||
<corners android:radius="12dp" />
|
||||
<!-- 轻微阴影(增强层次感) -->
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#00000008" /> <!-- 透明黑色阴影,避免生硬 -->
|
||||
</shape>
|
||||
|
||||
20
positions/src/main/res/drawable/bg_task_item.xml
Normal file
20
positions/src/main/res/drawable/bg_task_item.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/white" />
|
||||
|
||||
<corners android:radius="4dp" />
|
||||
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/gray" />
|
||||
|
||||
<padding
|
||||
android:left="8dp"
|
||||
android:top="8dp"
|
||||
android:right="8dp"
|
||||
android:bottom="8dp" />
|
||||
|
||||
</shape>
|
||||
|
||||
18
positions/src/main/res/drawable/btn_cancel_bg.xml
Normal file
18
positions/src/main/res/drawable/btn_cancel_bg.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 按压状态(加深灰色) -->
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#666666"/>
|
||||
<corners android:radius="4dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<!-- 默认状态(浅灰色) -->
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#999999"/>
|
||||
<corners android:radius="4dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
|
||||
18
positions/src/main/res/drawable/btn_confirm_bg.xml
Normal file
18
positions/src/main/res/drawable/btn_confirm_bg.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 按压状态(加深蓝色) -->
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#303F9F"/>
|
||||
<corners android:radius="4dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<!-- 默认状态(标准蓝色) -->
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#3F51B5"/>
|
||||
<corners android:radius="4dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
|
||||
16
positions/src/main/res/drawable/btn_delete_bg.xml
Normal file
16
positions/src/main/res/drawable/btn_delete_bg.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#D32F2F"/>
|
||||
<corners android:radius="4dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#F44336"/>
|
||||
<corners android:radius="4dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user