增加app列表页面,app数据存入数据库,优化icon cache

This commit is contained in:
2025-10-31 09:37:13 +08:00
parent 56341f6888
commit 5cc7caf34f
33 changed files with 1930 additions and 115 deletions

View File

@@ -43,6 +43,10 @@
android:name=".activity.contact.list.ContactListActivity" android:name=".activity.contact.list.ContactListActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity
android:name=".activity.app.AppListActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<provider <provider

View File

@@ -0,0 +1,73 @@
package com.ttstd.dialer.activity.app;
import androidx.lifecycle.Observer;
import androidx.loader.app.LoaderManager;
import androidx.recyclerview.widget.GridLayoutManager;
import com.tencent.mmkv.MMKV;
import com.ttstd.dialer.R;
import com.ttstd.dialer.adapter.MoreAppAdapter;
import com.ttstd.dialer.base.mvvm.BaseMvvmActivity;
import com.ttstd.dialer.db.app.DesktopSortApp;
import com.ttstd.dialer.config.CommonConfig;
import com.ttstd.dialer.databinding.ActivityAppListBinding;
import java.util.List;
public class AppListActivity extends BaseMvvmActivity<AppListViewModel, ActivityAppListBinding> {
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<List<DesktopSortApp>>() {
@Override
public void onChanged(List<DesktopSortApp> desktopSortApps) {
mMoreAppAdapter.setDesktopSortApps(desktopSortApps);
}
});
// mViewModel.getLauncherAppList();
mViewModel.getDbAppList();
}
public class BtnClick {
}
}

View File

@@ -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<ActivityAppListBinding, ActivityEvent> {
private static final String TAG = "AppListViewModel";
private AppRepository mAppRepository;
@Override
public void setContext(Context context) {
super.setContext(context);
mAppRepository = new AppRepository(context);
}
public MutableLiveData<List<DesktopSortApp>> mDesktopSortAppData = new MutableLiveData<>();
public void getDbAppList(){
Observable.fromCallable(new Callable<List<DesktopSortApp>>() {
@Override
public List<DesktopSortApp> call() throws Exception {
return mAppRepository.getAllContacts();
}
}) .compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<List<DesktopSortApp>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
Log.e("getDbAppList", "onSubscribe: " );
}
@Override
public void onNext(@NonNull List<DesktopSortApp> 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<List<ResolveInfo>>() {
@Override
public List<ResolveInfo> call() throws Exception {
return ApkUtils.getAllLauncherResolveInfo(getSafeContext());
}
})
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
.subscribeOn(Schedulers.io())
.map(new Function<List<ResolveInfo>, List<ComponentName>>() {
@Override
public List<ComponentName> apply(List<ResolveInfo> resolveInfos) throws Throwable {
List<ComponentName> componentNames = resolveInfos.stream().map(new java.util.function.Function<ResolveInfo, ComponentName>() {
@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<ComponentName>, List<DesktopSortApp>>() {
@Override
public List<DesktopSortApp> apply(List<ComponentName> componentNames) throws Throwable {
List<DesktopSortApp> desktopSortApps = componentNames.stream().map(new java.util.function.Function<ComponentName, DesktopSortApp>() {
@Override
public DesktopSortApp apply(ComponentName componentName) {
DesktopSortApp desktopSortApp = new DesktopSortApp(getSafeContext(), componentName);
return desktopSortApp;
}
}).sorted(new Comparator<DesktopSortApp>() {
@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<List<DesktopSortApp>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
}
@Override
public void onNext(@NonNull List<DesktopSortApp> desktopSortApps) {
Log.e(TAG, "getLauncherAppList" + "onNext: " + desktopSortApps);
mDesktopSortAppData.setValue(desktopSortApps);
}
@Override
public void onError(@NonNull Throwable e) {
}
@Override
public void onComplete() {
}
});
}
}

View File

@@ -102,7 +102,7 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
@Override @Override
protected void initData() { protected void initData() {
mViewModel.updateAppList();
} }
@Override @Override

View File

@@ -1,9 +1,112 @@
package com.ttstd.dialer.activity.main; package com.ttstd.dialer.activity.main;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ResolveInfo;
import android.util.Log;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.ActivityEvent; import com.trello.rxlifecycle4.android.ActivityEvent;
import com.ttstd.dialer.base.mvvm.BaseViewModel; import com.ttstd.dialer.base.mvvm.BaseViewModel;
import com.ttstd.dialer.databinding.ActivityMainBinding; import com.ttstd.dialer.databinding.ActivityMainBinding;
import com.ttstd.dialer.db.app.AppRepository;
import com.ttstd.dialer.db.app.DesktopSortApp;
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 MainViewModel extends BaseViewModel<ActivityMainBinding, ActivityEvent> { public class MainViewModel extends BaseViewModel<ActivityMainBinding, ActivityEvent> {
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<List<ResolveInfo>>() {
@Override
public List<ResolveInfo> call() throws Exception {
return ApkUtils.getAllLauncherResolveInfo(getSafeContext());
}
})
.compose(RxLifecycle.bindUntilEvent(getLifecycle(), ActivityEvent.DESTROY))
.subscribeOn(Schedulers.io())
.map(new Function<List<ResolveInfo>, List<ComponentName>>() {
@Override
public List<ComponentName> apply(List<ResolveInfo> resolveInfos) throws Throwable {
List<ComponentName> componentNames = resolveInfos.stream().map(new java.util.function.Function<ResolveInfo, ComponentName>() {
@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<ComponentName>, List<DesktopSortApp>>() {
@Override
public List<DesktopSortApp> apply(List<ComponentName> componentNames) throws Throwable {
List<DesktopSortApp> desktopSortApps = componentNames.stream().map(new java.util.function.Function<ComponentName, DesktopSortApp>() {
@Override
public DesktopSortApp apply(ComponentName componentName) {
DesktopSortApp desktopSortApp = new DesktopSortApp(getSafeContext(), componentName);
return desktopSortApp;
}
}).sorted(new Comparator<DesktopSortApp>() {
@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<List<DesktopSortApp>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
}
@Override
public void onNext(@NonNull List<DesktopSortApp> desktopSortApps) {
Log.e(TAG, "getLauncherAppList" + "onNext: " + desktopSortApps);
}
@Override
public void onError(@NonNull Throwable e) {
}
@Override
public void onComplete() {
}
});
}
} }

View File

@@ -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<MoreAppAdapter.AppHolder> implements LoaderManager.LoaderCallbacks<Drawable> {
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<DesktopSortApp> selectApps) {
}
private List<DesktopSortApp> mDesktopSortApps;
public void setDesktopSortApps(List<DesktopSortApp> 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<Drawable> 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<Drawable> loader, Drawable data) {
int position = loader.getId();
notifyItemChanged(position);
}
@Override
public void onLoaderReset(@NonNull @NotNull Loader<Drawable> 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);
}
}
}

View File

@@ -16,7 +16,9 @@ import com.tencent.bugly.crashreport.CrashReport;
import com.tencent.mmkv.MMKV; import com.tencent.mmkv.MMKV;
import com.ttstd.dialer.BuildConfig; import com.ttstd.dialer.BuildConfig;
import com.ttstd.dialer.config.CommonConfig; import com.ttstd.dialer.config.CommonConfig;
import com.ttstd.dialer.manager.AppManager;
import com.ttstd.dialer.utils.SystemUtils; import com.ttstd.dialer.utils.SystemUtils;
import com.ttstd.iconloader.IconCacheManager;
public class BaseApplication extends Application { public class BaseApplication extends Application {
@@ -67,6 +69,9 @@ public class BaseApplication extends Application {
CrashReport.initCrashReport(getApplicationContext(), "845e3ed68c", false); CrashReport.initCrashReport(getApplicationContext(), "845e3ed68c", false);
CrashReport.setDeviceId(this, Build.MODEL); CrashReport.setDeviceId(this, Build.MODEL);
xcrash.XCrash.init(this); xcrash.XCrash.init(this);
AppManager.init(this);
IconCacheManager.init(this);
} }
} }

View File

@@ -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<DesktopSortApp> 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<DesktopSortApp> 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<DesktopSortApp> searchApp(String searchQuery);
}

View File

@@ -1,4 +1,4 @@
package com.ttstd.dialer.db.contact; package com.ttstd.dialer.db.app;
import android.content.Context; import android.content.Context;
@@ -8,9 +8,9 @@ import androidx.room.RoomDatabase;
import java.io.File; 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 class AppDatabase extends RoomDatabase {
public abstract ContactDao contactDao(); public abstract AppDao appDao();
private static volatile AppDatabase INSTANCE; private static volatile AppDatabase INSTANCE;
@@ -20,7 +20,7 @@ public abstract class AppDatabase extends RoomDatabase {
synchronized (AppDatabase.class) { synchronized (AppDatabase.class) {
if (INSTANCE == null) { if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(), 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() // 为了简化示例允许主线程查询 // .allowMainThreadQueries() // 为了简化示例允许主线程查询
.build(); .build();
} }

View File

@@ -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<DesktopSortApp> getAllContacts() {
return mAppDao.getAllApp();
}
// 根据ID获取APP
public DesktopSortApp getContactById(int id) {
return mAppDao.getAppById(id);
}
// 搜索APP
public List<DesktopSortApp> searchContacts(String query) {
return mAppDao.searchApp("%" + query + "%");
}
// 添加APP
public long insert(DesktopSortApp contact) {
return mAppDao.insert(contact);
}
public long[] insert(List<DesktopSortApp> 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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -9,7 +9,7 @@ public class ContactRepository {
// 构造函数,获取数据库访问对象 // 构造函数,获取数据库访问对象
public ContactRepository(Context context) { public ContactRepository(Context context) {
AppDatabase db = AppDatabase.getDatabase(context); ContactDatabase db = ContactDatabase.getDatabase(context);
mContactDao = db.contactDao(); mContactDao = db.contactDao();
} }

View File

@@ -16,6 +16,7 @@ import androidx.fragment.app.Fragment;
import com.hjq.toast.Toaster; import com.hjq.toast.Toaster;
import com.ttstd.dialer.R; import com.ttstd.dialer.R;
import com.ttstd.dialer.activity.app.AppListActivity;
import com.ttstd.dialer.activity.contact.list.ContactListActivity; import com.ttstd.dialer.activity.contact.list.ContactListActivity;
import com.ttstd.dialer.base.mvvm.fragment.BaseMvvmFragment; import com.ttstd.dialer.base.mvvm.fragment.BaseMvvmFragment;
import com.ttstd.dialer.databinding.FragmentHomeBinding; import com.ttstd.dialer.databinding.FragmentHomeBinding;
@@ -186,12 +187,7 @@ public class HomeFragment extends BaseMvvmFragment<HomeViewModel, FragmentHomeBi
public class BtnClick { public class BtnClick {
public void openContact(View view) { public void openContact(View view) {
Intent intent = new Intent(mContext, ContactListActivity.class); startActivity(new Intent(mContext, ContactListActivity.class));
try {
startActivity(intent);
} catch (Exception e) {
Log.e(TAG, "openSettings: " + e.getMessage());
}
} }
public void openSettings(View view) { public void openSettings(View view) {
@@ -234,6 +230,11 @@ public class HomeFragment extends BaseMvvmFragment<HomeViewModel, FragmentHomeBi
openAppStore("com.tencent.mm"); openAppStore("com.tencent.mm");
} }
} }
public void openAppList(View view) {
startActivity(new Intent(mContext, AppListActivity.class));
}
} }
} }

View File

@@ -0,0 +1,38 @@
package com.ttstd.dialer.gson;
import android.content.Context;
import android.content.res.AssetManager;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* <读取Json文件的工具类>
*
* @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();
}
}

View File

@@ -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> T toJavaObject(JsonElement node, Class<T> clazz) {
return gson.fromJson(node, clazz);
}
public static <T> T toJavaObject(JsonElement node, Type type) {
return gson.fromJson(node, type);
}
public static <T> T toJavaObject(JsonElement node, TypeToken<?> typeToken) {
return toJavaObject(node, typeToken.getType());
}
public static <E> List<E> toJavaList(JsonElement node, Class<E> clazz) {
return toJavaObject(node, makeJavaType(List.class, clazz));
}
public static List<Object> toJavaList(JsonElement node) {
return toJavaObject(node, new TypeToken<List<Object>>() {
}.getType());
}
public static <V> Map<String, V> toJavaMap(JsonElement node, Class<V> clazz) {
return toJavaObject(node, makeJavaType(Map.class, String.class, clazz));
}
public static Map<String, Object> toJavaMap(JsonElement node) {
return toJavaObject(node, new TypeToken<Map<String, Object>>() {
}.getType());
}
public static <T> T toJavaObject(String content, Class<T> clazz) {
JsonObject jsonObject = getJsonObject(content);
String jsonString = jsonObject.toString();
return gson.fromJson(jsonString, clazz);
}
public static <T> T toJavaObject(String content, Type type) {
return gson.fromJson(content, type);
}
public static <T> T toJavaObject(String content, TypeToken<?> typeToken) {
return toJavaObject(content, typeToken.getType());
}
public static <E> List<E> toJavaList(String content, Class<E> clazz) {
return toJavaObject(content, makeJavaType(List.class, clazz));
}
public static List<Object> toJavaList(String content) {
return toJavaObject(content, new TypeToken<List<Object>>() {
}.getType());
}
public static <V> Map<String, V> toJavaMap(String content, Class<V> clazz) {
return toJavaObject(content, makeJavaType(Map.class, String.class, clazz));
}
public static Map<String, Object> toJavaMap(String content) {
return toJavaObject(content, new TypeToken<Map<String, Object>>() {
}.getType());
}
}

View File

@@ -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<Integer>, JsonDeserializer<Integer> {
@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);
}
}

View File

@@ -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<T> implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<T> rawType = (Class<T>) type.getRawType();
if (rawType != String.class) {
return null;
}
return (TypeAdapter<T>) new StringAdapter();
}
public static class StringAdapter extends TypeAdapter<String> {
@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);
}
}
}

View File

@@ -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<String> DEFAULT_APP_PACKAGES = new ArrayList<String>() {{
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<DesktopSortApp> getAllDesktopSortApps() {
try {
List<DesktopSortApp> result = mAppRepository.getAllContacts();
return result != null ? result : Collections.emptyList();
} catch (Exception e) {
Log.e("AppListProcessor", "获取桌面应用列表失败", e);
return Collections.emptyList();
}
}
private Optional<List<DesktopSortApp>> getAllDesktopSortAppsSafe() {
try {
return Optional.ofNullable(mAppRepository.getAllContacts())
.filter(list -> !list.isEmpty());
} catch (Exception e) {
Log.e("AppListProcessor", "获取应用列表失败", e);
return Optional.empty();
}
}
// 第二步:处理未安装的应用(删除操作) - 增加空值检查
private CompletableFuture<Void> processUninstalledApps(List<DesktopSortApp> desktopSortApps) {
return CompletableFuture.runAsync(() -> {
if (desktopSortApps == null || desktopSortApps.isEmpty()) {
Log.w("AppListProcessor", "桌面应用列表为空,跳过删除处理");
return;
}
List<Integer> 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<Void> processNewApps() {
return CompletableFuture.runAsync(() -> {
try {
List<ResolveInfo> resolveInfos = ApkUtils.getAllLauncherResolveInfo(mContext);
if (resolveInfos.isEmpty()) {
Log.w("AppListProcessor", "未获取到启动器应用列表");
return;
}
List<DesktopSortApp> 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<DesktopSortApp>() {
@Override
public int compare(DesktopSortApp o1, DesktopSortApp o2) {
return Collator.getInstance(Locale.CHINESE).compare(o1.getLabel(), o2.getLabel());
}
})
.collect(Collectors.toList());
List<DesktopSortApp> 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<Void> updatePosition() {
return CompletableFuture.runAsync(() -> {
try {
List<DesktopSortApp> desktopSortApps = getAllDesktopSortApps();
if (desktopSortApps == null || desktopSortApps.isEmpty()) {
Log.w("AppListProcessor", "无应用数据,跳过位置更新");
return;
}
// 使用Lambda简化代码
List<DesktopSortApp> 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<ResolveInfo> resolveInfos = ApkUtils.getResolveInfoByPackageName(mContext, packageName);
if (!resolveInfos.isEmpty()) {
resolveInfos.stream().map(new Function<ResolveInfo, DesktopSortApp>() {
@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<DesktopSortApp>() {
@Override
public boolean test(DesktopSortApp desktopSortApp) {
return mAppRepository.checkAppInfoExists(desktopSortApp.getPackageName(), desktopSortApp.getClassName()) <= 0;
}
}).forEach(new Consumer<DesktopSortApp>() {
@Override
public void accept(DesktopSortApp desktopSortApp) {
desktopSortApp.setPosition(mAppRepository.getTotalCount());
mAppRepository.insert(desktopSortApp);
}
});
}
}
public void getAllApp() {
List<DesktopSortApp> desktopSortApps = mAppRepository.getAllContacts();
List<Integer> ids = desktopSortApps.stream().filter(new Predicate<DesktopSortApp>() {
@Override
public boolean test(DesktopSortApp desktopSortApp) {
return !ApkUtils.isInstalled(mContext, desktopSortApp.getPackageName());
}
}).map(new java.util.function.Function<DesktopSortApp, Integer>() {
@Override
public Integer apply(DesktopSortApp desktopSortApp) {
return mAppRepository.getIdByPackageAndClass(desktopSortApp.getPackageName(), desktopSortApp.getClassName());
}
}).collect(Collectors.toList());
ids.stream().forEach(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
mAppRepository.deleteById(integer);
}
});
List<ResolveInfo> resolveInfos = ApkUtils.getAllLauncherResolveInfo(mContext);
List<DesktopSortApp> appList = resolveInfos.stream().map(new Function<ResolveInfo, ComponentName>() {
@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<ComponentName, DesktopSortApp>() {
@Override
public DesktopSortApp apply(ComponentName componentName) {
DesktopSortApp desktopSortApp = new DesktopSortApp(mContext, componentName);
return desktopSortApp;
}
}).collect(Collectors.toList());
List<DesktopSortApp> filterApp = appList.stream().filter(new Predicate<DesktopSortApp>() {
@Override
public boolean test(DesktopSortApp desktopSortApp) {
return ApkUtils.isInstalled(mContext, desktopSortApp.getPackageName());
}
}).filter(new Predicate<DesktopSortApp>() {
@Override
public boolean test(DesktopSortApp desktopSortApp) {
return mAppRepository.checkAppInfoExists(desktopSortApp.getPackageName(), desktopSortApp.getClassName()) > 0;
}
}).collect(Collectors.toList());
filterApp.stream().forEach(new Consumer<DesktopSortApp>() {
@Override
public void accept(DesktopSortApp desktopSortApp) {
mAppRepository.insert(desktopSortApp);
}
});
}
public String getDefaultAppList() {
PackageManager packageManager = mContext.getPackageManager();
List<ResolveInfo> resolveInfos = ApkUtils.getAllLauncherResolveInfo(mContext);
List<ResolveInfo> filter = resolveInfos.stream().filter(new Predicate<ResolveInfo>() {
@Override
public boolean test(ResolveInfo resolveInfo) {
return DEFAULT_APP_PACKAGES.contains(resolveInfo.activityInfo.packageName);
}
}).sorted(new Comparator<ResolveInfo>() {
@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<DesktopSortApp> desktopSortApps = filter.stream().map(new Function<ResolveInfo, DesktopSortApp>() {
@Override
public DesktopSortApp apply(ResolveInfo resolveInfo) {
ComponentName componentName = new ComponentName(resolveInfo.activityInfo.packageName,
resolveInfo.activityInfo.name);
return new DesktopSortApp(mContext, componentName);
}
}).sorted(new Comparator<DesktopSortApp>() {
@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);
}
}

View File

@@ -3,18 +3,21 @@ package com.ttstd.dialer.utils;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import com.hjq.toast.Toaster; import com.hjq.toast.Toaster;
import com.ttstd.dialer.R;
import java.util.List; import java.util.List;
public class ApkUtils { public class ApkUtils {
private static final String TAG = "ApkUtils"; private static final String TAG = "ApkUtils";
public static boolean isInstalled(Context context, String packageName) { public static boolean isInstalled(Context context, String packageName) {
@@ -83,4 +86,94 @@ public class ApkUtils {
intent.setComponent(new ComponentName(packageName, mainAct)); intent.setComponent(new ComponentName(packageName, mainAct));
return intent; 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<ResolveInfo> getAllLauncherResolveInfo(Context context) {
PackageManager packageManager = context.getPackageManager();
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
// 查询所有包含CATEGORY_LAUNCHER的Activity
List<ResolveInfo> resolveInfoList = packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
return resolveInfoList;
}
public static List<ResolveInfo> 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<ResolveInfo> 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;
}
} }

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:pathData="M271.96,64c-115.28,0 -208.87,93.67 -208.87,208.95C63.08,388.34 156.63,481.9 272.02,481.9h144.89c35.35,0 63.92,-28.76 63.92,-64.11L480.83,272.89c-0.01,-115.37 -93.5,-208.89 -208.87,-208.89zM416.88,417.79v0.03a0.93,0.93 0,0 1,0.04 0.14L272.02,417.96a145.24,145.24 0,1 1,144.86 -145.06v144.89zM416.9,542.23L272.02,542.23c-115.37,0 -208.9,93.52 -208.9,208.89 0,115.28 93.56,208.89 208.86,208.89 115.37,0 208.86,-93.53 208.86,-208.9v-144.89c-0.02,-35.35 -28.59,-63.98 -63.94,-63.98zM416.88,751.1a144.72,144.72 0,0 1,-88.48 133.5C310.56,892.15 291.6,895.88 272.02,895.88c-19.56,0 -38.52,-3.74 -56.35,-11.29A144.88,144.88 0,0 1,272.02 606.17h144.92a0.09,0.09 0,0 0,-0.04 0v144.92zM751.1,542.23h-144.89A63.97,63.97 0,0 0,542.23 606.21v144.89c0,115.37 93.52,208.9 208.89,208.9 115.28,0 208.89,-93.61 208.89,-208.89 -0,-115.37 -93.53,-208.89 -208.9,-208.89zM884.69,807.45a144.93,144.93 0,0 1,-278.52 -56.35L606.17,606.17h144.92A145,145 0,0 1,884.69 807.45zM606.21,481.9h144.89c115.37,0 208.9,-93.56 208.9,-208.95 0,-115.28 -93.61,-208.92 -208.89,-208.92 -115.37,0 -208.89,93.5 -208.89,208.87v144.89c0,35.35 28.63,64.11 63.98,64.11zM606.17,272.89A144.95,144.95 0,1 1,751.1 417.96L606.17,417.96a0.52,0.52 0,0 1,0 -0.14L606.17,272.89z"
android:fillColor="#8BC34A"/>
</vector>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.app.AppListActivity">
<data>
<variable
name="click"
type="com.ttstd.dialer.activity.app.AppListActivity.BtnClick" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -27,7 +27,7 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="3" android:layout_weight="2"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
@@ -181,6 +181,7 @@
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:src="@drawable/ic_douyin" android:src="@drawable/ic_douyin"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@@ -250,6 +251,101 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="horizontal">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="horizontal">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.shehuan.niv.NiceImageView
android:id="@+id/niceImageView5"
android:layout_width="100dp"
android:layout_height="100dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:maxLines="1"
android:singleLine="true"
android:textColor="@color/black"
android:textSize="@dimen/home_item_text_size"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/niceImageView5"
app:layout_constraintStart_toStartOf="@+id/niceImageView5"
app:layout_constraintTop_toBottomOf="@+id/niceImageView5" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:onClick="@{click::openAppList}"
android:orientation="horizontal">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.shehuan.niv.NiceImageView
android:id="@+id/niceImageView6"
android:layout_width="100dp"
android:layout_height="100dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:src="@drawable/ic_app"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:maxLines="1"
android:singleLine="true"
android:text="应用"
android:textColor="@color/black"
android:textSize="@dimen/home_item_text_size"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/niceImageView6"
app:layout_constraintStart_toStartOf="@+id/niceImageView6"
app:layout_constraintTop_toBottomOf="@+id/niceImageView6" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="120dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/iv_icon"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginTop="8dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_app_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
android:text="appname"
android:textColor="@color/black"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,4 +1,4 @@
package com.uiui.iconloader; package com.ttstd.iconloader;
import android.content.Context; import android.content.Context;

View File

@@ -1 +1 @@
<manifest package="com.uiui.iconloader" /> <manifest package="com.ttstd.iconloader" />

View File

@@ -1,4 +1,4 @@
package com.uiui.iconloader; package com.ttstd.iconloader;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;

View File

@@ -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<String, Drawable> 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<String, Drawable>(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<ResolveInfo> 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;
}
}

View File

@@ -1,4 +1,4 @@
package com.uiui.iconloader; package com.ttstd.iconloader;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
@@ -12,29 +12,24 @@ import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.util.Log;
import androidx.loader.content.AsyncTaskLoader; import androidx.loader.content.AsyncTaskLoader;
public class IconLoader extends AsyncTaskLoader<Drawable> { public class IconLoader extends AsyncTaskLoader<Drawable> {
private final ComponentName mComponentName; // 关键使用ComponentName标识组件 private static final String TAG = "IconLoader";
private final IconCacheManager mCache; 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); super(context);
mComponentName = componentName; this.mComponentName = componentName;
mCache = cache; this.mCache = iconCacheManager;
} }
@Override @Override
protected void onStartLoading() { protected void onStartLoading() {
String key = CacheKeyGenerator.generateKey(mComponentName);
// 1. 检查内存缓存
Drawable cachedIcon = mCache.getFromMemory(key);
if (cachedIcon != null) {
deliverResult(cachedIcon); // 直接返回缓存
return;
}
forceLoad(); // 触发后台加载
} }
@Override @Override
@@ -43,12 +38,15 @@ public class IconLoader extends AsyncTaskLoader<Drawable> {
// 1. 检查内存缓存 // 1. 检查内存缓存
Drawable cachedIcon = mCache.getFromMemory(key); Drawable cachedIcon = mCache.getFromMemory(key);
if (cachedIcon != null) return cachedIcon; if (cachedIcon != null) {
return cachedIcon;
}
// 2. 检查磁盘缓存 // 2. 检查磁盘缓存
Drawable diskCached = mCache.getFromDisk(key); Drawable diskCached = mCache.getFromDisk(key);
if (diskCached != null) { if (diskCached != null) {
mCache.putToMemory(key, diskCached); mCache.putToMemory(key, diskCached);
Log.e(TAG, "loadInBackground: putToMemory key = " + key);
return diskCached; return diskCached;
} }
@@ -59,12 +57,12 @@ public class IconLoader extends AsyncTaskLoader<Drawable> {
Drawable rawIcon = info.loadIcon(pm); // 加载该组件的图标 Drawable rawIcon = info.loadIcon(pm); // 加载该组件的图标
// 4. 图标处理压缩/主题适配 // 4. 图标处理压缩/主题适配
Drawable processedIcon = processIcon(rawIcon); // Drawable processedIcon = processIcon(rawIcon);
// 5. 更新缓存 // 5. 更新缓存
mCache.putToMemory(key, processedIcon); mCache.putToMemory(key, rawIcon);
mCache.putToDisk(key, processedIcon); mCache.putToDisk(key, rawIcon);
return processedIcon; return rawIcon;
} catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) {
return null; return null;
} }

View File

@@ -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通常也受支持
}
}

View File

@@ -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<String, Drawable> mMemoryCache;
private DiskLruCache mDiskCache;
public IconCacheManager(Context context) {
// 内存缓存最大8MB
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Drawable>(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);
}
}
}

View File

@@ -1,4 +1,4 @@
package com.uiui.iconloader; package com.ttstd.iconloader;
import org.junit.Test; import org.junit.Test;