From 5cc7caf34faae02ed3bf7f8eb78a39150f1acd3e Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Fri, 31 Oct 2025 09:37:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0app=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=EF=BC=8Capp=E6=95=B0=E6=8D=AE=E5=AD=98?= =?UTF-8?q?=E5=85=A5=E6=95=B0=E6=8D=AE=E5=BA=93=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?icon=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 4 + .../dialer/activity/app/AppListActivity.java | 73 ++++ .../dialer/activity/app/AppListViewModel.java | 153 +++++++ .../dialer/activity/main/MainActivity.java | 2 +- .../dialer/activity/main/MainViewModel.java | 103 +++++ .../ttstd/dialer/adapter/MoreAppAdapter.java | 122 ++++++ .../ttstd/dialer/base/BaseApplication.java | 5 + .../java/com/ttstd/dialer/db/app/AppDao.java | 57 +++ .../db/{contact => app}/AppDatabase.java | 8 +- .../ttstd/dialer/db/app/AppRepository.java | 78 ++++ .../dialer/db/app/ComponentNameConverter.java | 27 ++ .../ttstd/dialer/db/app/DesktopSortApp.java | 118 ++++++ .../dialer/db/contact/ContactDatabase.java | 31 ++ .../dialer/db/contact/ContactRepository.java | 2 +- .../dialer/fragment/home/HomeFragment.java | 13 +- .../ttstd/dialer/gson/GetJsonDataUtil.java | 38 ++ .../java/com/ttstd/dialer/gson/GsonUtils.java | 153 +++++++ .../dialer/gson/IntegerDefault0Adapter.java | 35 ++ .../gson/NullStringToEmptyAdapterFactory.java | 45 ++ .../com/ttstd/dialer/manager/AppManager.java | 384 ++++++++++++++++++ .../java/com/ttstd/dialer/utils/ApkUtils.java | 95 ++++- app/src/main/res/drawable/ic_app.xml | 9 + app/src/main/res/layout/activity_app_list.xml | 25 ++ app/src/main/res/layout/fragment_home.xml | 98 ++++- app/src/main/res/layout/item_app.xml | 49 +++ .../iconloader/ExampleInstrumentedTest.java | 2 +- iconloader/src/main/AndroidManifest.xml | 2 +- .../iconloader/CacheKeyGenerator.java | 2 +- .../ttstd/iconloader/IconCacheManager.java | 168 ++++++++ .../iconloader/IconLoader.java | 34 +- .../java/com/ttstd/iconloader/MD5Util.java | 29 ++ .../com/uiui/iconloader/IconCacheManager.java | 79 ---- .../iconloader/ExampleUnitTest.java | 2 +- 33 files changed, 1930 insertions(+), 115 deletions(-) create mode 100644 app/src/main/java/com/ttstd/dialer/activity/app/AppListActivity.java create mode 100644 app/src/main/java/com/ttstd/dialer/activity/app/AppListViewModel.java create mode 100644 app/src/main/java/com/ttstd/dialer/adapter/MoreAppAdapter.java create mode 100644 app/src/main/java/com/ttstd/dialer/db/app/AppDao.java rename app/src/main/java/com/ttstd/dialer/db/{contact => app}/AppDatabase.java (79%) create mode 100644 app/src/main/java/com/ttstd/dialer/db/app/AppRepository.java create mode 100644 app/src/main/java/com/ttstd/dialer/db/app/ComponentNameConverter.java create mode 100644 app/src/main/java/com/ttstd/dialer/db/app/DesktopSortApp.java create mode 100644 app/src/main/java/com/ttstd/dialer/db/contact/ContactDatabase.java create mode 100644 app/src/main/java/com/ttstd/dialer/gson/GetJsonDataUtil.java create mode 100644 app/src/main/java/com/ttstd/dialer/gson/GsonUtils.java create mode 100644 app/src/main/java/com/ttstd/dialer/gson/IntegerDefault0Adapter.java create mode 100644 app/src/main/java/com/ttstd/dialer/gson/NullStringToEmptyAdapterFactory.java create mode 100644 app/src/main/java/com/ttstd/dialer/manager/AppManager.java create mode 100644 app/src/main/res/drawable/ic_app.xml create mode 100644 app/src/main/res/layout/activity_app_list.xml create mode 100644 app/src/main/res/layout/item_app.xml rename iconloader/src/androidTest/java/com/{uiui => ttstd}/iconloader/ExampleInstrumentedTest.java (96%) rename iconloader/src/main/java/com/{uiui => ttstd}/iconloader/CacheKeyGenerator.java (94%) create mode 100644 iconloader/src/main/java/com/ttstd/iconloader/IconCacheManager.java rename iconloader/src/main/java/com/{uiui => ttstd}/iconloader/IconLoader.java (79%) create mode 100644 iconloader/src/main/java/com/ttstd/iconloader/MD5Util.java delete mode 100644 iconloader/src/main/java/com/uiui/iconloader/IconCacheManager.java rename iconloader/src/test/java/com/{uiui => ttstd}/iconloader/ExampleUnitTest.java (92%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6e31bb9..d048109 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,6 +43,10 @@ android:name=".activity.contact.list.ContactListActivity" android:launchMode="singleTask" android:screenOrientation="portrait" /> + { + private static final String TAG = "AppListActivity"; + + private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + + private MoreAppAdapter mMoreAppAdapter; + + + @Override + public boolean setNightMode() { + return true; + } + + @Override + public boolean setfitWindow() { + return true; + } + + @Override + protected int getLayoutId() { + return R.layout.activity_app_list; + } + + @Override + protected void initDataBinding() { + mViewModel.setContext(this); + mViewModel.setVDBinding(mViewDataBinding); + mViewModel.setLifecycle(getLifecycleSubject()); + mViewDataBinding.setClick(new BtnClick()); + } + + @Override + protected void initView() { + mMoreAppAdapter = new MoreAppAdapter(LoaderManager.getInstance(this)); + mViewDataBinding.recyclerView.setLayoutManager(new GridLayoutManager(this, 3)); + mViewDataBinding.recyclerView.setAdapter(mMoreAppAdapter); + + } + + @Override + protected void initData() { + mViewModel.mDesktopSortAppData.observe(this, new Observer>() { + @Override + public void onChanged(List desktopSortApps) { + mMoreAppAdapter.setDesktopSortApps(desktopSortApps); + } + }); + +// mViewModel.getLauncherAppList(); + mViewModel.getDbAppList(); + } + + + public class BtnClick { + + } +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/app/AppListViewModel.java b/app/src/main/java/com/ttstd/dialer/activity/app/AppListViewModel.java new file mode 100644 index 0000000..dfd27ac --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/activity/app/AppListViewModel.java @@ -0,0 +1,153 @@ +package com.ttstd.dialer.activity.app; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.util.Log; + +import androidx.lifecycle.MutableLiveData; + +import com.trello.rxlifecycle4.RxLifecycle; +import com.trello.rxlifecycle4.android.ActivityEvent; +import com.ttstd.dialer.base.mvvm.BaseViewModel; +import com.ttstd.dialer.db.app.AppRepository; +import com.ttstd.dialer.db.app.DesktopSortApp; +import com.ttstd.dialer.databinding.ActivityAppListBinding; +import com.ttstd.dialer.utils.ApkUtils; + +import java.text.Collator; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class AppListViewModel extends BaseViewModel { + private static final String TAG = "AppListViewModel"; + + private AppRepository mAppRepository; + + @Override + public void setContext(Context context) { + super.setContext(context); + mAppRepository = new AppRepository(context); + } + + public MutableLiveData> mDesktopSortAppData = new MutableLiveData<>(); + + public void getDbAppList(){ + Observable.fromCallable(new Callable>() { + @Override + public List call() throws Exception { + return mAppRepository.getAllContacts(); + } + }) .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + Log.e("getDbAppList", "onSubscribe: " ); + } + + @Override + public void onNext(@NonNull List desktopSortApps) { + Log.e("getDbAppList", "onNext: "+desktopSortApps ); + mDesktopSortAppData.setValue(desktopSortApps); + } + + @Override + public void onError(@NonNull Throwable e) { + Log.e("getDbAppList", "onError: " +e.getMessage()); + } + + @Override + public void onComplete() { + Log.e("getDbAppList", "onComplete: " ); + } + }); + + + } + + public void getLauncherAppList() { + Observable.fromCallable(new Callable>() { + @Override + public List call() throws Exception { + return ApkUtils.getAllLauncherResolveInfo(getSafeContext()); + } + }) + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + .subscribeOn(Schedulers.io()) + .map(new Function, List>() { + @Override + public List apply(List resolveInfos) throws Throwable { + List componentNames = resolveInfos.stream().map(new java.util.function.Function() { + @Override + public ComponentName apply(ResolveInfo resolveInfo) { + String packageName = resolveInfo.activityInfo.packageName; + String className = resolveInfo.activityInfo.name; + ComponentName componentName = new ComponentName(packageName, className); + return componentName; + } + }).collect(Collectors.toList()); + return componentNames; + } + }) + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + .subscribeOn(Schedulers.io()) + .map(new Function, List>() { + @Override + public List apply(List componentNames) throws Throwable { + List desktopSortApps = componentNames.stream().map(new java.util.function.Function() { + @Override + public DesktopSortApp apply(ComponentName componentName) { + DesktopSortApp desktopSortApp = new DesktopSortApp(getSafeContext(), componentName); + return desktopSortApp; + } + }).sorted(new Comparator() { + @Override + public int compare(DesktopSortApp o1, DesktopSortApp o2) { + return Collator.getInstance(Locale.CHINESE).compare(o1.getLabel(), o2.getLabel()); + } + }).collect(Collectors.toList()); + return desktopSortApps; + } + }) + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + + } + + @Override + public void onNext(@NonNull List desktopSortApps) { + Log.e(TAG, "getLauncherAppList" + "onNext: " + desktopSortApps); + mDesktopSortAppData.setValue(desktopSortApps); + } + + @Override + public void onError(@NonNull Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + + } + +} diff --git a/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java b/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java index 16bcece..3bac6dd 100644 --- a/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java +++ b/app/src/main/java/com/ttstd/dialer/activity/main/MainActivity.java @@ -102,7 +102,7 @@ public class MainActivity extends BaseMvvmActivity { + private static final String TAG = "MainViewModel"; + + private AppRepository mAppRepository; + + @Override + public void setContext(Context context) { + super.setContext(context); + mAppRepository = new AppRepository(context); + } + + public void updateAppList() { + Observable.fromCallable(new Callable>() { + @Override + public List call() throws Exception { + return ApkUtils.getAllLauncherResolveInfo(getSafeContext()); + } + }) + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + .subscribeOn(Schedulers.io()) + .map(new Function, List>() { + @Override + public List apply(List resolveInfos) throws Throwable { + List componentNames = resolveInfos.stream().map(new java.util.function.Function() { + @Override + public ComponentName apply(ResolveInfo resolveInfo) { + String packageName = resolveInfo.activityInfo.packageName; + String className = resolveInfo.activityInfo.name; + ComponentName componentName = new ComponentName(packageName, className); + return componentName; + } + }).collect(Collectors.toList()); + return componentNames; + } + }) + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + .subscribeOn(Schedulers.io()) + .map(new Function, List>() { + @Override + public List apply(List componentNames) throws Throwable { + List desktopSortApps = componentNames.stream().map(new java.util.function.Function() { + @Override + public DesktopSortApp apply(ComponentName componentName) { + DesktopSortApp desktopSortApp = new DesktopSortApp(getSafeContext(), componentName); + return desktopSortApp; + } + }).sorted(new Comparator() { + @Override + public int compare(DesktopSortApp o1, DesktopSortApp o2) { + return Collator.getInstance(Locale.CHINESE).compare(o1.getLabel(), o2.getLabel()); + } + }).collect(Collectors.toList()); + return desktopSortApps; + } + }) + .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + + } + + @Override + public void onNext(@NonNull List desktopSortApps) { + Log.e(TAG, "getLauncherAppList" + "onNext: " + desktopSortApps); + } + + @Override + public void onError(@NonNull Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + } } diff --git a/app/src/main/java/com/ttstd/dialer/adapter/MoreAppAdapter.java b/app/src/main/java/com/ttstd/dialer/adapter/MoreAppAdapter.java new file mode 100644 index 0000000..b864704 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/adapter/MoreAppAdapter.java @@ -0,0 +1,122 @@ +package com.ttstd.dialer.adapter; + +import android.content.ComponentName; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.fragment.app.FragmentActivity; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; +import androidx.recyclerview.widget.RecyclerView; + +import com.tencent.mmkv.MMKV; +import com.ttstd.dialer.R; +import com.ttstd.dialer.db.app.DesktopSortApp; +import com.ttstd.dialer.config.CommonConfig; +import com.ttstd.dialer.utils.ApkUtils; +import com.ttstd.iconloader.IconCacheManager; +import com.ttstd.iconloader.IconLoader; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class MoreAppAdapter extends RecyclerView.Adapter implements LoaderManager.LoaderCallbacks { + private static final String TAG = "MoreAppAdapter"; + + private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + + private FragmentActivity mContext; + private LoaderManager mLoaderManager; + private IconCacheManager mIconCacheManager = IconCacheManager.getInstance(); + + public MoreAppAdapter(LoaderManager loaderManager) { + mLoaderManager = loaderManager; + } + + public void setSelectApps(List selectApps) { + + } + + private List mDesktopSortApps; + + public void setDesktopSortApps(List desktopSortApps) { + mDesktopSortApps = desktopSortApps; + notifyDataSetChanged(); + } + + @NonNull + @NotNull + @Override + public AppHolder onCreateViewHolder(@NonNull @NotNull ViewGroup parent, int viewType) { + mContext = (FragmentActivity) parent.getContext(); + return new AppHolder(LayoutInflater.from(mContext).inflate(R.layout.item_app, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull @NotNull AppHolder holder, int position) { + DesktopSortApp desktopSortApp = mDesktopSortApps.get(position); + Drawable drawable = mIconCacheManager.getIcon(desktopSortApp.getComponentName().flattenToShortString()); + if (drawable != null) { + holder.iv_icon.setImageDrawable(drawable); + } else { + mLoaderManager.restartLoader(position, null, this).forceLoad(); + } + holder.tv_app_name.setText(desktopSortApp.getLabel()); + holder.root.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ApkUtils.openApp(mContext, desktopSortApp.getComponentName()); + } + }); + } + + @Override + public int getItemCount() { + return mDesktopSortApps == null ? 0 : mDesktopSortApps.size(); + } + + @NonNull + @NotNull + @Override + public Loader onCreateLoader(int id, @Nullable @org.jetbrains.annotations.Nullable Bundle args) { + DesktopSortApp appInfo = mDesktopSortApps.get(id); + ComponentName componentName = appInfo.getComponentName(); // 从数据项中获取 ComponentName + return new IconLoader(mContext, componentName, mIconCacheManager); + } + + @Override + public void onLoadFinished(@NonNull @NotNull Loader loader, Drawable data) { + int position = loader.getId(); + notifyItemChanged(position); + } + + @Override + public void onLoaderReset(@NonNull @NotNull Loader loader) { + Log.e(TAG, "onLoaderReset: "); + } + + public class AppHolder extends RecyclerView.ViewHolder { + + ConstraintLayout root; + ImageView iv_icon; + TextView tv_app_name; + + public AppHolder(@NonNull @NotNull View itemView) { + super(itemView); + root = itemView.findViewById(R.id.root); + iv_icon = itemView.findViewById(R.id.iv_icon); + tv_app_name = itemView.findViewById(R.id.tv_app_name); + } + } + +} diff --git a/app/src/main/java/com/ttstd/dialer/base/BaseApplication.java b/app/src/main/java/com/ttstd/dialer/base/BaseApplication.java index 97a2ff4..91c5be7 100644 --- a/app/src/main/java/com/ttstd/dialer/base/BaseApplication.java +++ b/app/src/main/java/com/ttstd/dialer/base/BaseApplication.java @@ -16,7 +16,9 @@ import com.tencent.bugly.crashreport.CrashReport; import com.tencent.mmkv.MMKV; import com.ttstd.dialer.BuildConfig; import com.ttstd.dialer.config.CommonConfig; +import com.ttstd.dialer.manager.AppManager; import com.ttstd.dialer.utils.SystemUtils; +import com.ttstd.iconloader.IconCacheManager; public class BaseApplication extends Application { @@ -67,6 +69,9 @@ public class BaseApplication extends Application { CrashReport.initCrashReport(getApplicationContext(), "845e3ed68c", false); CrashReport.setDeviceId(this, Build.MODEL); xcrash.XCrash.init(this); + + AppManager.init(this); + IconCacheManager.init(this); } } diff --git a/app/src/main/java/com/ttstd/dialer/db/app/AppDao.java b/app/src/main/java/com/ttstd/dialer/db/app/AppDao.java new file mode 100644 index 0000000..049ff76 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/db/app/AppDao.java @@ -0,0 +1,57 @@ +package com.ttstd.dialer.db.app; + +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.Query; +import androidx.room.Update; + +import java.util.List; + +@Dao +public interface AppDao { + // 基本查询:获取总行数 + @Query("SELECT COUNT(*) FROM app_list") + Integer getTotalCount(); + + // 查询:根据特定列的非空值计数 + @Query("SELECT COUNT(id) FROM app_list") + Integer getCountById(); + + @Query("SELECT * FROM app_list WHERE package_name = :packageName AND class_name = :className") + DesktopSortApp getAppInfo(String packageName, String className); + + @Query("SELECT id FROM app_list WHERE package_name = :packageName AND class_name = :className") + Integer getIdByPackageAndClass(String packageName, String className); + + // 检查数据是否存在(返回布尔值) + @Query("SELECT COUNT(*) FROM app_list WHERE package_name = :pkgName AND class_name = :clsName") + Integer checkAppInfoExists(String pkgName, String clsName); + + @Insert + long insert(DesktopSortApp desktopSortApp); + + @Insert + long[] insert(List contacts); + + @Update + Integer update(DesktopSortApp desktopSortApp); + + @Delete + Integer delete(DesktopSortApp desktopSortApp); + + @Query("DELETE FROM app_list WHERE id = :id") + Integer deleteById(Integer id); + + @Query("DELETE FROM app_list") + Integer deleteAll(); + + @Query("SELECT * FROM app_list ORDER BY position ASC") + List getAllApp(); + + @Query("SELECT * FROM app_list WHERE id = :id") + DesktopSortApp getAppById(Integer id); + + @Query("SELECT * FROM app_list WHERE label LIKE :searchQuery OR package_name LIKE :searchQuery") + List searchApp(String searchQuery); +} diff --git a/app/src/main/java/com/ttstd/dialer/db/contact/AppDatabase.java b/app/src/main/java/com/ttstd/dialer/db/app/AppDatabase.java similarity index 79% rename from app/src/main/java/com/ttstd/dialer/db/contact/AppDatabase.java rename to app/src/main/java/com/ttstd/dialer/db/app/AppDatabase.java index 9b152d6..19e1251 100644 --- a/app/src/main/java/com/ttstd/dialer/db/contact/AppDatabase.java +++ b/app/src/main/java/com/ttstd/dialer/db/app/AppDatabase.java @@ -1,4 +1,4 @@ -package com.ttstd.dialer.db.contact; +package com.ttstd.dialer.db.app; import android.content.Context; @@ -8,9 +8,9 @@ import androidx.room.RoomDatabase; import java.io.File; -@Database(entities = {Contact.class}, version = 1, exportSchema = false) +@Database(entities = {DesktopSortApp.class}, version = 2, exportSchema = false) public abstract class AppDatabase extends RoomDatabase { - public abstract ContactDao contactDao(); + public abstract AppDao appDao(); private static volatile AppDatabase INSTANCE; @@ -20,7 +20,7 @@ public abstract class AppDatabase extends RoomDatabase { synchronized (AppDatabase.class) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), - AppDatabase.class, context.getExternalFilesDir("db") + File.separator + "contact_database") + AppDatabase.class, context.getExternalFilesDir("db") + File.separator + "app" + File.separator + "app_db") // .allowMainThreadQueries() // 为了简化示例,允许主线程查询 .build(); } diff --git a/app/src/main/java/com/ttstd/dialer/db/app/AppRepository.java b/app/src/main/java/com/ttstd/dialer/db/app/AppRepository.java new file mode 100644 index 0000000..699d22f --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/db/app/AppRepository.java @@ -0,0 +1,78 @@ +package com.ttstd.dialer.db.app; + +import android.content.Context; + +import java.util.List; + +public class AppRepository { + AppDao mAppDao; + + public AppRepository(Context context) { + AppDatabase db = AppDatabase.getDatabase(context); + mAppDao = db.appDao(); + } + + public int getTotalCount() { + return mAppDao.getTotalCount(); + } + + public int getCountById() { + return mAppDao.getCountById(); + } + + public DesktopSortApp getAppInfo(String packageName, String className) { + return mAppDao.getAppInfo(packageName, className); + } + + public int getIdByPackageAndClass(String packageName, String className) { + return mAppDao.getIdByPackageAndClass(packageName, className); + } + + public int checkAppInfoExists(String packageName, String className) { + return mAppDao.checkAppInfoExists(packageName, className); + } + + // 获取所有APP + public List getAllContacts() { + return mAppDao.getAllApp(); + } + + // 根据ID获取APP + public DesktopSortApp getContactById(int id) { + return mAppDao.getAppById(id); + } + + // 搜索APP + public List searchContacts(String query) { + return mAppDao.searchApp("%" + query + "%"); + } + + // 添加APP + public long insert(DesktopSortApp contact) { + return mAppDao.insert(contact); + } + + public long[] insert(List contacts) { + return mAppDao.insert(contacts); + } + + // 更新APP + public int update(DesktopSortApp contact) { + return mAppDao.update(contact); + } + + // 删除APP + public int delete(DesktopSortApp contact) { + return mAppDao.delete(contact); + } + + // 根据ID删除APP + public int deleteById(int id) { + return mAppDao.deleteById(id); + } + + // 删除所有APP + public int deleteAll() { + return mAppDao.deleteAll(); + } +} diff --git a/app/src/main/java/com/ttstd/dialer/db/app/ComponentNameConverter.java b/app/src/main/java/com/ttstd/dialer/db/app/ComponentNameConverter.java new file mode 100644 index 0000000..58536ba --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/db/app/ComponentNameConverter.java @@ -0,0 +1,27 @@ +package com.ttstd.dialer.db.app; + +import android.content.ComponentName; +import androidx.room.TypeConverter; + +public class ComponentNameConverter { + + @TypeConverter + public static String fromComponentName(ComponentName componentName) { + // 如果 componentName 为 null,则返回 null + if (componentName == null) { + return null; + } + // 将 ComponentName 转换为其字符串形式(例如:"com.example.app/.MyService") + return componentName.flattenToString(); + } + + @TypeConverter + public static ComponentName toComponentName(String componentNameString) { + // 如果字符串为 null 或空,则返回 null + if (componentNameString == null || componentNameString.isEmpty()) { + return null; + } + // 从字符串形式解包为 ComponentName 对象 + return ComponentName.unflattenFromString(componentNameString); + } +} diff --git a/app/src/main/java/com/ttstd/dialer/db/app/DesktopSortApp.java b/app/src/main/java/com/ttstd/dialer/db/app/DesktopSortApp.java new file mode 100644 index 0000000..0dd1967 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/db/app/DesktopSortApp.java @@ -0,0 +1,118 @@ +package com.ttstd.dialer.db.app; + +import android.content.ComponentName; +import android.content.Context; + +import androidx.annotation.Nullable; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.Index; +import androidx.room.PrimaryKey; +import androidx.room.TypeConverters; + +import com.ttstd.dialer.utils.ApkUtils; + +import java.io.Serializable; +import java.util.Objects; + +@Entity(tableName = "app_list", indices = {@Index(value = "component_name", unique = true)}) +@TypeConverters({ComponentNameConverter.class}) +public class DesktopSortApp implements Serializable { + private static final long serialVersionUID = 9113517079637096245L; + + @PrimaryKey(autoGenerate = true) + private int id; + @ColumnInfo(name = "component_name") + private ComponentName componentName; + @ColumnInfo(name = "label") + private String mLabel; + @ColumnInfo(name = "package_name") + private String mPackageName; + @ColumnInfo(name = "class_name") + private String mClassName; + @ColumnInfo(name = "position") + private int mPosition; + + public DesktopSortApp() { + } + + public DesktopSortApp(Context context, ComponentName componentName) { + this.componentName = componentName; + mPackageName = componentName.getPackageName(); + mClassName = componentName.getClassName(); + mPosition = 0; + mLabel = ApkUtils.getAppName(context, componentName); + } + + public DesktopSortApp(Context context, ComponentName componentName, int pos) { + this.componentName = componentName; + mPackageName = componentName.getPackageName(); + mClassName = componentName.getClassName(); + mPosition = pos; + mLabel = ApkUtils.getAppName(context, componentName); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public ComponentName getComponentName() { + return componentName; + } + + public void setComponentName(ComponentName componentName) { + this.componentName = componentName; + } + + public String getLabel() { + return mLabel; + } + + public void setLabel(String label) { + this.mLabel = label; + } + + public String getPackageName() { + return mPackageName; + } + + public void setPackageName(String packageName) { + this.mPackageName = packageName; + } + + public String getClassName() { + return mClassName; + } + + public void setClassName(String className) { + this.mClassName = className; + } + + public int getPosition() { + return mPosition; + } + + public void setPosition(int position) { + this.mPosition = position; + } + + @Override + public boolean equals(@Nullable @org.jetbrains.annotations.Nullable Object obj) { + if (obj instanceof DesktopSortApp) { + return Objects.equals(componentName, ((DesktopSortApp) obj).componentName); + } else if (obj instanceof ComponentName) { + return Objects.equals(componentName, obj); + } else { + return false; + } + } + + @Override + public int hashCode() { + return componentName.hashCode(); + } +} diff --git a/app/src/main/java/com/ttstd/dialer/db/contact/ContactDatabase.java b/app/src/main/java/com/ttstd/dialer/db/contact/ContactDatabase.java new file mode 100644 index 0000000..1eb148d --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/db/contact/ContactDatabase.java @@ -0,0 +1,31 @@ +package com.ttstd.dialer.db.contact; + +import android.content.Context; + +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; + +import java.io.File; + +@Database(entities = {Contact.class}, version = 1, exportSchema = false) +public abstract class ContactDatabase extends RoomDatabase { + public abstract ContactDao contactDao(); + + private static volatile ContactDatabase INSTANCE; + + // 单例模式获取数据库实例 + public static ContactDatabase getDatabase(final Context context) { + if (INSTANCE == null) { + synchronized (ContactDatabase.class) { + if (INSTANCE == null) { + INSTANCE = Room.databaseBuilder(context.getApplicationContext(), + ContactDatabase.class, context.getExternalFilesDir("db") + File.separator + "contact" + File.separator + "contact_database") +// .allowMainThreadQueries() // 为了简化示例,允许主线程查询 + .build(); + } + } + } + return INSTANCE; + } +} diff --git a/app/src/main/java/com/ttstd/dialer/db/contact/ContactRepository.java b/app/src/main/java/com/ttstd/dialer/db/contact/ContactRepository.java index 6bade65..97e8ff3 100644 --- a/app/src/main/java/com/ttstd/dialer/db/contact/ContactRepository.java +++ b/app/src/main/java/com/ttstd/dialer/db/contact/ContactRepository.java @@ -9,7 +9,7 @@ public class ContactRepository { // 构造函数,获取数据库访问对象 public ContactRepository(Context context) { - AppDatabase db = AppDatabase.getDatabase(context); + ContactDatabase db = ContactDatabase.getDatabase(context); mContactDao = db.contactDao(); } diff --git a/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java b/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java index ac56cfa..8ad2888 100644 --- a/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java +++ b/app/src/main/java/com/ttstd/dialer/fragment/home/HomeFragment.java @@ -16,6 +16,7 @@ import androidx.fragment.app.Fragment; import com.hjq.toast.Toaster; import com.ttstd.dialer.R; +import com.ttstd.dialer.activity.app.AppListActivity; import com.ttstd.dialer.activity.contact.list.ContactListActivity; import com.ttstd.dialer.base.mvvm.fragment.BaseMvvmFragment; import com.ttstd.dialer.databinding.FragmentHomeBinding; @@ -186,12 +187,7 @@ public class HomeFragment extends BaseMvvmFragment + * + * @author: 小嵩 + * @date: 2017/3/16 16:22 + + */ + +public class GetJsonDataUtil { + + + public String getJson(Context context, String fileName) { + + StringBuilder stringBuilder = new StringBuilder(); + try { + AssetManager assetManager = context.getAssets(); + BufferedReader bf = new BufferedReader(new InputStreamReader( + assetManager.open(fileName))); + String line; + while ((line = bf.readLine()) != null) { + stringBuilder.append(line); + } + } catch (IOException e) { + e.printStackTrace(); + } + return stringBuilder.toString(); + } +} + diff --git a/app/src/main/java/com/ttstd/dialer/gson/GsonUtils.java b/app/src/main/java/com/ttstd/dialer/gson/GsonUtils.java new file mode 100644 index 0000000..fd7f6b6 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/gson/GsonUtils.java @@ -0,0 +1,153 @@ +package com.ttstd.dialer.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.Objects; + + +public class GsonUtils { + //https://blog.csdn.net/zte1055889498/article/details/122400299 + + public static JsonObject getJsonObject(String jsonString) { + JsonObject jsonObject = JsonParser.parseString(jsonString).getAsJsonObject(); + return jsonObject; + } + + private static final Gson gson; + private static final Gson exposeGson; + + static { + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapterFactory(new NullStringToEmptyAdapterFactory()); + builder.registerTypeAdapter(Integer.class, new IntegerDefault0Adapter()); + builder.registerTypeAdapter(int.class, new IntegerDefault0Adapter()); + builder.disableHtmlEscaping(); + builder.enableComplexMapKeySerialization(); + // builder.excludeFieldsWithoutExposeAnnotation(); + builder.setDateFormat("yyyy-MM-dd HH:mm:ss"); + gson = builder.create(); + exposeGson = new GsonBuilder() + .excludeFieldsWithoutExposeAnnotation() + .create(); + } + + public static Type makeJavaType(Type rawType, Type... typeArguments) { + return TypeToken.getParameterized(rawType, typeArguments).getType(); + } + + public static String toString(Object value) { + if (Objects.isNull(value)) { + return null; + } + if (value instanceof String) { + return (String) value; + } + return toJSONString(value); + } + + public static String toJSONString(Object value) { + return gson.toJson(value); + } + + public static String toExposeJSONString(Object value) { + return gson.toJson(value); + } + + + public static String toPrettyString(Object value) { + return gson.newBuilder().setPrettyPrinting().create().toJson(value); + } + + public static JsonElement fromJavaObject(Object value) { + JsonElement result = null; + if (Objects.nonNull(value) && (value instanceof String)) { + result = parseObject((String) value); + } else { + result = gson.toJsonTree(value); + } + return result; + } + + public static JsonElement parseObject(String content) { + return JsonParser.parseString(content); + } + + public static JsonElement getJsonElement(JsonObject node, String name) { + return node.get(name); + } + + public static JsonElement getJsonElement(JsonArray node, int index) { + return node.get(index); + } + + public static T toJavaObject(JsonElement node, Class clazz) { + return gson.fromJson(node, clazz); + } + + public static T toJavaObject(JsonElement node, Type type) { + return gson.fromJson(node, type); + } + + public static T toJavaObject(JsonElement node, TypeToken typeToken) { + return toJavaObject(node, typeToken.getType()); + } + + public static List toJavaList(JsonElement node, Class clazz) { + return toJavaObject(node, makeJavaType(List.class, clazz)); + } + + public static List toJavaList(JsonElement node) { + return toJavaObject(node, new TypeToken>() { + }.getType()); + } + + public static Map toJavaMap(JsonElement node, Class clazz) { + return toJavaObject(node, makeJavaType(Map.class, String.class, clazz)); + } + + public static Map toJavaMap(JsonElement node) { + return toJavaObject(node, new TypeToken>() { + }.getType()); + } + + public static T toJavaObject(String content, Class clazz) { + JsonObject jsonObject = getJsonObject(content); + String jsonString = jsonObject.toString(); + return gson.fromJson(jsonString, clazz); + } + + public static T toJavaObject(String content, Type type) { + return gson.fromJson(content, type); + } + + public static T toJavaObject(String content, TypeToken typeToken) { + return toJavaObject(content, typeToken.getType()); + } + + public static List toJavaList(String content, Class clazz) { + return toJavaObject(content, makeJavaType(List.class, clazz)); + } + + public static List toJavaList(String content) { + return toJavaObject(content, new TypeToken>() { + }.getType()); + } + + public static Map toJavaMap(String content, Class clazz) { + return toJavaObject(content, makeJavaType(Map.class, String.class, clazz)); + } + + public static Map toJavaMap(String content) { + return toJavaObject(content, new TypeToken>() { + }.getType()); + } +} diff --git a/app/src/main/java/com/ttstd/dialer/gson/IntegerDefault0Adapter.java b/app/src/main/java/com/ttstd/dialer/gson/IntegerDefault0Adapter.java new file mode 100644 index 0000000..fb5c0c5 --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/gson/IntegerDefault0Adapter.java @@ -0,0 +1,35 @@ +package com.ttstd.dialer.gson; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.JsonSyntaxException; + +import java.lang.reflect.Type; + +public class IntegerDefault0Adapter implements JsonSerializer, JsonDeserializer { + @Override + public Integer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + try { + if (json.getAsString().equals("")) { + return 0; + } + } catch (Exception ignore) { + } + try { + return json.getAsInt(); + } catch (NumberFormatException e) { + throw new JsonSyntaxException(e); + } + } + + @Override + public JsonElement serialize(Integer src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ttstd/dialer/gson/NullStringToEmptyAdapterFactory.java b/app/src/main/java/com/ttstd/dialer/gson/NullStringToEmptyAdapterFactory.java new file mode 100644 index 0000000..183e98b --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/gson/NullStringToEmptyAdapterFactory.java @@ -0,0 +1,45 @@ +package com.ttstd.dialer.gson; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +public class NullStringToEmptyAdapterFactory implements TypeAdapterFactory { + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + + Class rawType = (Class) type.getRawType(); + if (rawType != String.class) { + return null; + } + return (TypeAdapter) new StringAdapter(); + } + + public static class StringAdapter extends TypeAdapter { + @Override + public String read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return ""; + } + return reader.nextString(); + } + + @Override + public void write(JsonWriter writer, String value) throws IOException { + if (value == null) { + writer.nullValue(); + return; + } + writer.value(value); + } + } + +} + diff --git a/app/src/main/java/com/ttstd/dialer/manager/AppManager.java b/app/src/main/java/com/ttstd/dialer/manager/AppManager.java new file mode 100644 index 0000000..fee2faa --- /dev/null +++ b/app/src/main/java/com/ttstd/dialer/manager/AppManager.java @@ -0,0 +1,384 @@ +package com.ttstd.dialer.manager; + +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.util.Log; + +import com.tencent.mmkv.MMKV; +import com.ttstd.dialer.config.CommonConfig; +import com.ttstd.dialer.db.app.AppRepository; +import com.ttstd.dialer.db.app.DesktopSortApp; +import com.ttstd.dialer.gson.GsonUtils; +import com.ttstd.dialer.utils.ApkUtils; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + + +public class AppManager { + @SuppressLint("StaticFieldLeak") + private static AppManager INSTANCE; + private Context mContext; + private AppRepository mAppRepository; + + private MMKV mMMKV = MMKV.mmkvWithID(CommonConfig.MMKV_ID, MMKV.MULTI_PROCESS_MODE); + + private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4)); + private static final ExecutorService ASYNC_EXECUTOR = Executors.newFixedThreadPool(CORE_POOL_SIZE); + + + public static final List DEFAULT_APP_PACKAGES = new ArrayList() {{ + this.add("com.android.settings"); + this.add("com.android.dialer"); + this.add("com.android.camera2"); + this.add("com.tencent.mm"); + this.add("com.jiangjia.gif"); + this.add("com.ss.android.ugc.aweme"); + }}; + + public static void init(Context context) { + if (INSTANCE == null) { + INSTANCE = new AppManager(context); + } + } + + public static AppManager getInstance() { + if (INSTANCE == null) { + throw new IllegalStateException("You must be init AppManager first"); + } + return INSTANCE; + } + + private AppManager(Context context) { + this.mContext = context.getApplicationContext(); + this.mAppRepository = new AppRepository(context); + executeAppListProcessing(); + } + + public interface ProgressCallback { + void onProgress(String step, int current, int total); + + void onCompleted(boolean success, String message); + } + + // 异步执行方法 + public void executeAppListProcessing() { + CompletableFuture + .supplyAsync(this::getAllDesktopSortApps, ASYNC_EXECUTOR) + .thenComposeAsync(desktopSortApps -> { + // 判断获取的应用列表是否为空 + if (desktopSortApps == null || desktopSortApps.isEmpty()) { + Log.i("AppListProcessor", "检测到桌面应用列表为空,直接插入新应用数据"); + return processNewApps() + .thenComposeAsync(unused -> updatePosition(), ASYNC_EXECUTOR); + } else { + return processUninstalledApps(desktopSortApps) + .thenComposeAsync(unused -> processNewApps(), ASYNC_EXECUTOR) + .thenComposeAsync(unused -> updatePosition(), ASYNC_EXECUTOR); + } + }, ASYNC_EXECUTOR) + .thenComposeAsync(unused -> processNewApps(), ASYNC_EXECUTOR) + .exceptionally(throwable -> { + // 统一异常处理 + Log.e("AppListProcessor", "异步处理失败", throwable); + return null; + }); + } + + // 第一步:获取所有桌面应用(增加空值安全处理) + private List getAllDesktopSortApps() { + try { + List result = mAppRepository.getAllContacts(); + return result != null ? result : Collections.emptyList(); + } catch (Exception e) { + Log.e("AppListProcessor", "获取桌面应用列表失败", e); + return Collections.emptyList(); + } + } + + private Optional> getAllDesktopSortAppsSafe() { + try { + return Optional.ofNullable(mAppRepository.getAllContacts()) + .filter(list -> !list.isEmpty()); + } catch (Exception e) { + Log.e("AppListProcessor", "获取应用列表失败", e); + return Optional.empty(); + } + } + + // 第二步:处理未安装的应用(删除操作) - 增加空值检查 + private CompletableFuture processUninstalledApps(List desktopSortApps) { + return CompletableFuture.runAsync(() -> { + if (desktopSortApps == null || desktopSortApps.isEmpty()) { + Log.w("AppListProcessor", "桌面应用列表为空,跳过删除处理"); + return; + } + + List ids = desktopSortApps.stream() + .filter(desktopSortApp -> desktopSortApp != null && + !ApkUtils.isInstalled(mContext, desktopSortApp.getPackageName())) + .map(desktopSortApp -> { + try { + return mAppRepository.getIdByPackageAndClass( + desktopSortApp.getPackageName(), desktopSortApp.getClassName()); + } catch (Exception e) { + Log.e("AppListProcessor", "获取应用ID失败: " + desktopSortApp.getPackageName(), e); + return -1; // 返回无效ID,后续过滤掉 + } + }) + .filter(id -> id > 0) // 过滤掉无效ID + .collect(Collectors.toList()); + + if (!ids.isEmpty()) { + ids.forEach(id -> { + try { + mAppRepository.deleteById(id); + } catch (Exception e) { + Log.e("AppListProcessor", "删除应用失败, ID: " + id, e); + } + }); + Log.i("AppListProcessor", "成功删除 " + ids.size() + " 个未安装应用"); + } + }, ASYNC_EXECUTOR); + } + + // 第三步:处理新应用(插入操作) - 增加空值和异常处理 + private CompletableFuture processNewApps() { + return CompletableFuture.runAsync(() -> { + try { + List resolveInfos = ApkUtils.getAllLauncherResolveInfo(mContext); + if (resolveInfos.isEmpty()) { + Log.w("AppListProcessor", "未获取到启动器应用列表"); + return; + } + + List appList = resolveInfos.stream() + .filter(Objects::nonNull) + .map(resolveInfo -> { + try { + String packageName = resolveInfo.activityInfo.packageName; + String className = resolveInfo.activityInfo.name; + ComponentName componentName = new ComponentName(packageName, className); + return new DesktopSortApp(mContext, componentName); + } catch (Exception e) { + Log.e("AppListProcessor", "创建DesktopSortApp失败", e); + return null; + } + }) + .filter(Objects::nonNull) + .sorted(new Comparator() { + @Override + public int compare(DesktopSortApp o1, DesktopSortApp o2) { + return Collator.getInstance(Locale.CHINESE).compare(o1.getLabel(), o2.getLabel()); + } + }) + .collect(Collectors.toList()); + + List filterApp = appList.stream() + .filter(desktopSortApp -> ApkUtils.isInstalled(mContext, desktopSortApp.getPackageName())) + .filter(desktopSortApp -> { + try { + return mAppRepository.checkAppInfoExists( + desktopSortApp.getPackageName(), desktopSortApp.getClassName()) <= 0; + } catch (Exception e) { + Log.e("AppListProcessor", "检查应用存在性失败", e); + return false; + } + }) + .collect(Collectors.toList()); + + if (!filterApp.isEmpty()) { + filterApp.forEach(desktopSortApp -> { + desktopSortApp.setPosition(mAppRepository.getTotalCount()); + try { + mAppRepository.insert(desktopSortApp); + } catch (Exception e) { + Log.e("AppListProcessor", "插入应用失败: " + desktopSortApp.getPackageName(), e); + } + }); + Log.i("AppListProcessor", "成功插入 " + filterApp.size() + " 个新应用"); + } else { + Log.i("AppListProcessor", "没有需要插入的新应用"); + } + } catch (Exception e) { + Log.e("AppListProcessor", "处理新应用时发生异常", e); + } + }, ASYNC_EXECUTOR); + } + + // 第四步:更新应用位置(您提供的方法优化) + private CompletableFuture updatePosition() { + return CompletableFuture.runAsync(() -> { + try { + List desktopSortApps = getAllDesktopSortApps(); + if (desktopSortApps == null || desktopSortApps.isEmpty()) { + Log.w("AppListProcessor", "无应用数据,跳过位置更新"); + return; + } + + // 使用Lambda简化代码 + List sortedApps = desktopSortApps.stream() + .filter(Objects::nonNull) + .sorted((o1, o2) -> Integer.compare(o1.getPosition(), o2.getPosition())) + .collect(Collectors.toList()); + + // 批量更新位置 + IntStream.range(0, sortedApps.size()) + .forEach(index -> { + DesktopSortApp desktopSortApp = sortedApps.get(index); + desktopSortApp.setPosition(index); + try { + mAppRepository.update(desktopSortApp); + } catch (Exception e) { + Log.e("AppListProcessor", "更新应用位置失败: " + desktopSortApp.getPackageName(), e); + } + }); + Log.i("AppListProcessor", "成功更新 " + sortedApps.size() + " 个应用的位置"); + } catch (Exception e) { + Log.e("AppListProcessor", "更新应用位置时发生异常", e); + } + }, ASYNC_EXECUTOR); + } + + public void updateApp(String packageName) { + List resolveInfos = ApkUtils.getResolveInfoByPackageName(mContext, packageName); + if (!resolveInfos.isEmpty()) { + resolveInfos.stream().map(new Function() { + @Override + public DesktopSortApp apply(ResolveInfo resolveInfo) { + String packageName = resolveInfo.activityInfo.packageName; + String className = resolveInfo.activityInfo.name; + ComponentName componentName = new ComponentName(packageName, className); + return new DesktopSortApp(mContext, componentName); + } + }).filter(new Predicate() { + @Override + public boolean test(DesktopSortApp desktopSortApp) { + return mAppRepository.checkAppInfoExists(desktopSortApp.getPackageName(), desktopSortApp.getClassName()) <= 0; + } + }).forEach(new Consumer() { + @Override + public void accept(DesktopSortApp desktopSortApp) { + desktopSortApp.setPosition(mAppRepository.getTotalCount()); + mAppRepository.insert(desktopSortApp); + } + }); + } + } + + public void getAllApp() { + List desktopSortApps = mAppRepository.getAllContacts(); + List ids = desktopSortApps.stream().filter(new Predicate() { + @Override + public boolean test(DesktopSortApp desktopSortApp) { + return !ApkUtils.isInstalled(mContext, desktopSortApp.getPackageName()); + } + }).map(new java.util.function.Function() { + @Override + public Integer apply(DesktopSortApp desktopSortApp) { + return mAppRepository.getIdByPackageAndClass(desktopSortApp.getPackageName(), desktopSortApp.getClassName()); + } + }).collect(Collectors.toList()); + ids.stream().forEach(new Consumer() { + @Override + public void accept(Integer integer) { + mAppRepository.deleteById(integer); + } + }); + + List resolveInfos = ApkUtils.getAllLauncherResolveInfo(mContext); + List appList = resolveInfos.stream().map(new Function() { + @Override + public ComponentName apply(ResolveInfo resolveInfo) { + String packageName = resolveInfo.activityInfo.packageName; + String className = resolveInfo.activityInfo.name; + ComponentName componentName = new ComponentName(packageName, className); + return componentName; + } + }).map(new Function() { + @Override + public DesktopSortApp apply(ComponentName componentName) { + DesktopSortApp desktopSortApp = new DesktopSortApp(mContext, componentName); + return desktopSortApp; + } + }).collect(Collectors.toList()); + List filterApp = appList.stream().filter(new Predicate() { + @Override + public boolean test(DesktopSortApp desktopSortApp) { + return ApkUtils.isInstalled(mContext, desktopSortApp.getPackageName()); + } + }).filter(new Predicate() { + @Override + public boolean test(DesktopSortApp desktopSortApp) { + return mAppRepository.checkAppInfoExists(desktopSortApp.getPackageName(), desktopSortApp.getClassName()) > 0; + } + }).collect(Collectors.toList()); + + filterApp.stream().forEach(new Consumer() { + @Override + public void accept(DesktopSortApp desktopSortApp) { + mAppRepository.insert(desktopSortApp); + } + }); + + + } + + public String getDefaultAppList() { + PackageManager packageManager = mContext.getPackageManager(); + List resolveInfos = ApkUtils.getAllLauncherResolveInfo(mContext); + List filter = resolveInfos.stream().filter(new Predicate() { + @Override + public boolean test(ResolveInfo resolveInfo) { + return DEFAULT_APP_PACKAGES.contains(resolveInfo.activityInfo.packageName); + } + }).sorted(new Comparator() { + @Override + public int compare(ResolveInfo o1, ResolveInfo o2) { + return Collator.getInstance(Locale.CHINESE).compare(o1.activityInfo.loadLabel(packageManager), o2.activityInfo.loadLabel(packageManager)); + } + }).collect(Collectors.toList()); + + List desktopSortApps = filter.stream().map(new Function() { + @Override + public DesktopSortApp apply(ResolveInfo resolveInfo) { + ComponentName componentName = new ComponentName(resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name); + return new DesktopSortApp(mContext, componentName); + } + }).sorted(new Comparator() { + @Override + public int compare(DesktopSortApp o1, DesktopSortApp o2) { + return Boolean.compare(ApkUtils.isSystemApp(mContext, o2.getPackageName()), ApkUtils.isSystemApp(mContext, o1.getPackageName())); + } + }).collect(Collectors.toCollection(LinkedList::new)); + + for (int i = 0; i < desktopSortApps.size(); i++) { + DesktopSortApp desktopSortApp = desktopSortApps.get(i); + desktopSortApp.setPosition(i); + } + + return GsonUtils.toJSONString(desktopSortApps); + } + + +} diff --git a/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java b/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java index 1ac71fd..5fd6fa3 100644 --- a/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java +++ b/app/src/main/java/com/ttstd/dialer/utils/ApkUtils.java @@ -3,18 +3,21 @@ package com.ttstd.dialer.utils; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.Log; import com.hjq.toast.Toaster; +import com.ttstd.dialer.R; import java.util.List; public class ApkUtils { - private static final String TAG = "ApkUtils"; public static boolean isInstalled(Context context, String packageName) { @@ -83,4 +86,94 @@ public class ApkUtils { intent.setComponent(new ComponentName(packageName, mainAct)); return intent; } + + public static boolean openApp(Context context, ComponentName componentName) { + Intent intent = new Intent() + .setComponent(componentName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + context.startActivity(intent); + return true; + } catch (Exception e) { + Log.e(TAG, "openApp: " + e.getMessage()); + } + return false; + } + + public static Drawable getDrawableFromComponentName(Context context, ComponentName componentName) { + try { + PackageManager packageManager = context.getPackageManager(); + ActivityInfo activityInfo = packageManager.getActivityInfo(componentName, 0); + ApplicationInfo applicationInfo = activityInfo.applicationInfo; + Drawable activityIcon = applicationInfo.loadIcon(packageManager); + return activityIcon; + + } catch (PackageManager.NameNotFoundException e) { + // 处理未找到组件的情况 + e.printStackTrace(); + // 可以根据需要设置一个默认图标或提示用户 + } + return context.getDrawable(R.mipmap.ic_launcher); + } + + public static String getAppName(Context context, ComponentName componentName) { + try { + PackageManager packageManager = context.getPackageManager(); + ActivityInfo activityInfo = packageManager.getActivityInfo(componentName, 0); + ApplicationInfo applicationInfo = activityInfo.applicationInfo; + String name = applicationInfo.loadLabel(packageManager).toString(); + return name; + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + return "未知"; + } + + public static List getAllLauncherResolveInfo(Context context) { + PackageManager packageManager = context.getPackageManager(); + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + // 查询所有包含CATEGORY_LAUNCHER的Activity + List resolveInfoList = packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL); + return resolveInfoList; + } + + public static List getResolveInfoByPackageName(Context context, String packageName) { + PackageManager packageManager = context.getPackageManager(); // 在Activity中使用 + + // 构建查询Intent + Intent intent = new Intent(Intent.ACTION_MAIN, null); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setPackage(packageName); // 设置包名过滤 + + // 查询匹配的Activity列表 + List resolveInfoList = packageManager.queryIntentActivities(intent, 0); + return resolveInfoList; + } + + /** + * 判断是否为系统应用 + * + * @param context 上下文 + * @param pkgName 包名 + * @return + */ + public static boolean isSystemApp(Context context, String pkgName) { + boolean isSystemApp = false; + PackageInfo pi = null; + try { + PackageManager pm = context.getPackageManager(); + pi = pm.getPackageInfo(pkgName, 0); + } catch (PackageManager.NameNotFoundException e) { + Log.e("isSystemApp: ", "NameNotFoundException:" + e.getMessage()); + } + // 是系统中已安装的应用 + if (pi != null) { + boolean isSysApp = (pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 1; + boolean isSysUpd = (pi.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 1; + isSystemApp = isSysApp || isSysUpd; + } + return isSystemApp; + } + } diff --git a/app/src/main/res/drawable/ic_app.xml b/app/src/main/res/drawable/ic_app.xml new file mode 100644 index 0000000..1cd3479 --- /dev/null +++ b/app/src/main/res/drawable/ic_app.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_app_list.xml b/app/src/main/res/layout/activity_app_list.xml new file mode 100644 index 0000000..a6ca4f4 --- /dev/null +++ b/app/src/main/res/layout/activity_app_list.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index b4cc47f..a8310d7 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -27,7 +27,7 @@ @@ -250,6 +251,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_app.xml b/app/src/main/res/layout/item_app.xml new file mode 100644 index 0000000..7f32c74 --- /dev/null +++ b/app/src/main/res/layout/item_app.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/iconloader/src/androidTest/java/com/uiui/iconloader/ExampleInstrumentedTest.java b/iconloader/src/androidTest/java/com/ttstd/iconloader/ExampleInstrumentedTest.java similarity index 96% rename from iconloader/src/androidTest/java/com/uiui/iconloader/ExampleInstrumentedTest.java rename to iconloader/src/androidTest/java/com/ttstd/iconloader/ExampleInstrumentedTest.java index 3f077f7..e59a838 100644 --- a/iconloader/src/androidTest/java/com/uiui/iconloader/ExampleInstrumentedTest.java +++ b/iconloader/src/androidTest/java/com/ttstd/iconloader/ExampleInstrumentedTest.java @@ -1,4 +1,4 @@ -package com.uiui.iconloader; +package com.ttstd.iconloader; import android.content.Context; diff --git a/iconloader/src/main/AndroidManifest.xml b/iconloader/src/main/AndroidManifest.xml index 1720cbe..2a1c06a 100644 --- a/iconloader/src/main/AndroidManifest.xml +++ b/iconloader/src/main/AndroidManifest.xml @@ -1 +1 @@ - + diff --git a/iconloader/src/main/java/com/uiui/iconloader/CacheKeyGenerator.java b/iconloader/src/main/java/com/ttstd/iconloader/CacheKeyGenerator.java similarity index 94% rename from iconloader/src/main/java/com/uiui/iconloader/CacheKeyGenerator.java rename to iconloader/src/main/java/com/ttstd/iconloader/CacheKeyGenerator.java index 2109746..f813ced 100644 --- a/iconloader/src/main/java/com/uiui/iconloader/CacheKeyGenerator.java +++ b/iconloader/src/main/java/com/ttstd/iconloader/CacheKeyGenerator.java @@ -1,4 +1,4 @@ -package com.uiui.iconloader; +package com.ttstd.iconloader; import android.content.ComponentName; import android.content.Intent; diff --git a/iconloader/src/main/java/com/ttstd/iconloader/IconCacheManager.java b/iconloader/src/main/java/com/ttstd/iconloader/IconCacheManager.java new file mode 100644 index 0000000..45dedf3 --- /dev/null +++ b/iconloader/src/main/java/com/ttstd/iconloader/IconCacheManager.java @@ -0,0 +1,168 @@ +package com.ttstd.iconloader; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.os.Build; +import android.util.Log; + +import androidx.collection.LruCache; + +import com.jakewharton.disklrucache.DiskLruCache; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class IconCacheManager { + private static IconCacheManager INSTANCE; + + private static final String TAG = "IconCacheManager"; + private LruCache mMemoryCache; + private DiskLruCache mDiskCache; + + public static void init(Context context) { + if (context == null) { + throw new RuntimeException("Context is NULL"); + } + if (INSTANCE == null) { + INSTANCE = new IconCacheManager(context); + } + } + + public static IconCacheManager getInstance() { + if (INSTANCE == null) { + throw new IllegalStateException("You must be init IconCacheManager first"); + } + return INSTANCE; + } + + public IconCacheManager(Context context) { + // 内存缓存(最大8MB) + int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + int cacheSize = maxMemory / 8; + mMemoryCache = new LruCache(cacheSize) { + @Override + protected int sizeOf(String key, Drawable icon) { + if (icon instanceof BitmapDrawable) { + return ((BitmapDrawable) icon).getBitmap().getByteCount() / 1024; + } + return 1; // 非Bitmap对象按1KB计算 + } + }; + + // 磁盘缓存(50MB) + File cacheDir = new File(context.getExternalCacheDir(), "icon_cache"); + try { + mDiskCache = DiskLruCache.open(cacheDir, 1, 1, 50 * 1024 * 1024); + } catch (Exception e) { + Log.e(TAG, "IconCacheManager: " + e.getMessage()); + } + + final PackageManager packageManager = context.getPackageManager(); + ExecutorService executor = Executors.newFixedThreadPool(10); + executor.execute(new Runnable() { + @Override + public void run() { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + // 查询所有包含CATEGORY_LAUNCHER的Activity + List resolveInfoList = packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL); + for (ResolveInfo resolveInfo : resolveInfoList) { + ComponentName componentName = new ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name); + String key = componentName.flattenToShortString(); + try { + ActivityInfo info = packageManager.getActivityInfo(componentName, 0); + Drawable rawIcon = info.loadIcon(packageManager); + putToMemory(key, rawIcon); + putToDisk(key, rawIcon); + } catch (Exception e) { + Log.e(TAG, "run: " + e.getMessage()); + } + } + } + }); + } + + public Drawable getFromMemory(String key) { + return mMemoryCache.get(key); + } + + public Drawable getFromDisk(String key) { + String md5 = MD5Util.getMD5(key); + try { + DiskLruCache.Snapshot snapshot = mDiskCache.get(md5); + if (snapshot != null) { + InputStream in = snapshot.getInputStream(0); + return Drawable.createFromStream(in, null); + } + } catch (IOException e) { + Log.e(TAG, "getFromDisk Disk read error", e); + } + return null; + } + + public void putToMemory(String key, Drawable icon) { + mMemoryCache.put(key, icon); + } + + public void putToDisk(String key, Drawable icon) { + String md5 = MD5Util.getMD5(key); + try { + DiskLruCache.Editor editor = mDiskCache.edit(md5); + if (icon instanceof BitmapDrawable) { + Bitmap bitmap = ((BitmapDrawable) icon).getBitmap(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, editor.newOutputStream(0)); + editor.commit(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (icon instanceof AdaptiveIconDrawable) { + // 分别获取背景和前景Drawable + Drawable[] layers = new Drawable[2]; + layers[0] = ((AdaptiveIconDrawable) icon).getBackground(); + layers[1] = ((AdaptiveIconDrawable) icon).getForeground(); + LayerDrawable layerDrawable = new LayerDrawable(layers); + + int width = icon.getIntrinsicWidth(); + int height = icon.getIntrinsicHeight(); + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + layerDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + layerDrawable.draw(canvas); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, editor.newOutputStream(0)); + editor.commit(); + } + } + } catch (IOException e) { + Log.e(TAG, "putToDisk Disk write error", e); + } + } + + public Drawable getIcon(String key) { + Drawable memDrawable = getFromMemory(key); + if (memDrawable != null) { + Log.i(TAG, "getIcon: fromMemory " + key); + return memDrawable; + } + Drawable diskDrawable = getFromDisk(key); + if (diskDrawable != null) { + putToMemory(key, diskDrawable); + Log.i(TAG, "getIcon: fromDisk " + key); + return diskDrawable; + } + return null; + } +} \ No newline at end of file diff --git a/iconloader/src/main/java/com/uiui/iconloader/IconLoader.java b/iconloader/src/main/java/com/ttstd/iconloader/IconLoader.java similarity index 79% rename from iconloader/src/main/java/com/uiui/iconloader/IconLoader.java rename to iconloader/src/main/java/com/ttstd/iconloader/IconLoader.java index 2c31f76..39ee1ac 100644 --- a/iconloader/src/main/java/com/uiui/iconloader/IconLoader.java +++ b/iconloader/src/main/java/com/ttstd/iconloader/IconLoader.java @@ -1,4 +1,4 @@ -package com.uiui.iconloader; +package com.ttstd.iconloader; import android.content.ComponentName; import android.content.Context; @@ -12,29 +12,24 @@ import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Build; +import android.util.Log; import androidx.loader.content.AsyncTaskLoader; public class IconLoader extends AsyncTaskLoader { - private final ComponentName mComponentName; // 关键:使用ComponentName标识组件 + private static final String TAG = "IconLoader"; private final IconCacheManager mCache; + private ComponentName mComponentName; - public IconLoader(Context context, ComponentName componentName, IconCacheManager cache) { + public IconLoader(Context context, ComponentName componentName, IconCacheManager iconCacheManager) { super(context); - mComponentName = componentName; - mCache = cache; + this.mComponentName = componentName; + this.mCache = iconCacheManager; } @Override protected void onStartLoading() { - String key = CacheKeyGenerator.generateKey(mComponentName); - // 1. 检查内存缓存 - Drawable cachedIcon = mCache.getFromMemory(key); - if (cachedIcon != null) { - deliverResult(cachedIcon); // 直接返回缓存 - return; - } - forceLoad(); // 触发后台加载 + } @Override @@ -43,12 +38,15 @@ public class IconLoader extends AsyncTaskLoader { // 1. 检查内存缓存 Drawable cachedIcon = mCache.getFromMemory(key); - if (cachedIcon != null) return cachedIcon; + if (cachedIcon != null) { + return cachedIcon; + } // 2. 检查磁盘缓存 Drawable diskCached = mCache.getFromDisk(key); if (diskCached != null) { mCache.putToMemory(key, diskCached); + Log.e(TAG, "loadInBackground: putToMemory key = " + key); return diskCached; } @@ -59,12 +57,12 @@ public class IconLoader extends AsyncTaskLoader { Drawable rawIcon = info.loadIcon(pm); // 加载该组件的图标 // 4. 图标处理(压缩/主题适配) - Drawable processedIcon = processIcon(rawIcon); +// Drawable processedIcon = processIcon(rawIcon); // 5. 更新缓存 - mCache.putToMemory(key, processedIcon); - mCache.putToDisk(key, processedIcon); - return processedIcon; + mCache.putToMemory(key, rawIcon); + mCache.putToDisk(key, rawIcon); + return rawIcon; } catch (PackageManager.NameNotFoundException e) { return null; } diff --git a/iconloader/src/main/java/com/ttstd/iconloader/MD5Util.java b/iconloader/src/main/java/com/ttstd/iconloader/MD5Util.java new file mode 100644 index 0000000..537fb99 --- /dev/null +++ b/iconloader/src/main/java/com/ttstd/iconloader/MD5Util.java @@ -0,0 +1,29 @@ +package com.ttstd.iconloader; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class MD5Util { + public static String getMD5(String input) { + try { + // 1. 获取 MD5 消息摘要实例 + MessageDigest md = MessageDigest.getInstance("MD5"); + // 2. 将输入字符串转换为字节数组。建议指定编码(如UTF-8),但使用默认平台编码时通常可省略。 + byte[] inputBytes = input.getBytes(); // 如需指定编码,可使用 input.getBytes("UTF-8") + // 3. 计算消息摘要,得到128位(16字节)的哈希值 + byte[] digest = md.digest(inputBytes); + // 4. 将字节数组转换为十六进制字符串表示 + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + // 每个字节转换为两位十六进制数,不足两位前面补零 + sb.append(String.format("%02x", b & 0xff)); + } + // 5. 返回最终的32位十六进制字符串 + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + // MD5算法是所有Java标准平台必须支持的,此异常通常不会抛出 + throw new RuntimeException("MD5 algorithm not available", e); + } + // 若指定了编码(如UTF-8),需捕获 UnsupportedEncodingException,但UTF-8通常也受支持 + } +} \ No newline at end of file diff --git a/iconloader/src/main/java/com/uiui/iconloader/IconCacheManager.java b/iconloader/src/main/java/com/uiui/iconloader/IconCacheManager.java deleted file mode 100644 index b37a97c..0000000 --- a/iconloader/src/main/java/com/uiui/iconloader/IconCacheManager.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.uiui.iconloader; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.util.Log; - -import androidx.collection.LruCache; - -import com.jakewharton.disklrucache.DiskLruCache; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; - -public class IconCacheManager { - private static final String TAG = "IconCacheManager"; - private LruCache mMemoryCache; - private DiskLruCache mDiskCache; - - public IconCacheManager(Context context) { - // 内存缓存(最大8MB) - int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); - int cacheSize = maxMemory / 8; - mMemoryCache = new LruCache(cacheSize) { - @Override - protected int sizeOf(String key, Drawable icon) { - if (icon instanceof BitmapDrawable) { - return ((BitmapDrawable) icon).getBitmap().getByteCount() / 1024; - } - return 1; // 非Bitmap对象按1KB计算 - } - }; - - // 磁盘缓存(50MB) - File cacheDir = new File(context.getCacheDir(), "icon_cache"); - try { - mDiskCache = DiskLruCache.open(cacheDir, 1, 1, 50 * 1024 * 1024); - } catch (Exception e) { - Log.e(TAG, "IconCacheManager: " + e.getMessage()); - } - } - - public Drawable getFromMemory(String key) { - return mMemoryCache.get(key); - } - - public Drawable getFromDisk(String key) { - try { - DiskLruCache.Snapshot snapshot = mDiskCache.get(key); - if (snapshot != null) { - InputStream in = snapshot.getInputStream(0); - return Drawable.createFromStream(in, null); - } - } catch (IOException e) { - Log.e("IconCache", "Disk read error", e); - } - return null; - } - - public void putToMemory(String key, Drawable icon) { - mMemoryCache.put(key, icon); - } - - - public void putToDisk(String key, Drawable icon) { - try { - DiskLruCache.Editor editor = mDiskCache.edit(key); - if (icon instanceof BitmapDrawable) { - Bitmap bitmap = ((BitmapDrawable) icon).getBitmap(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, editor.newOutputStream(0)); - editor.commit(); - } - } catch (IOException e) { - Log.e("IconCache", "Disk write error", e); - } - } -} \ No newline at end of file diff --git a/iconloader/src/test/java/com/uiui/iconloader/ExampleUnitTest.java b/iconloader/src/test/java/com/ttstd/iconloader/ExampleUnitTest.java similarity index 92% rename from iconloader/src/test/java/com/uiui/iconloader/ExampleUnitTest.java rename to iconloader/src/test/java/com/ttstd/iconloader/ExampleUnitTest.java index 2e86c2d..0f9bea9 100644 --- a/iconloader/src/test/java/com/uiui/iconloader/ExampleUnitTest.java +++ b/iconloader/src/test/java/com/ttstd/iconloader/ExampleUnitTest.java @@ -1,4 +1,4 @@ -package com.uiui.iconloader; +package com.ttstd.iconloader; import org.junit.Test;