T inflateViewStub(@IdRes int id, @LayoutRes int layoutRes) {
View stub = mTBLauncherActivity.findViewById(id);
return (T) ViewStubPreview.inflateStub(stub, layoutRes);
}
private void updateClearButton() {
if (mSearchEditText.getText().length() > 0 || TBApplication.state().isResultListVisible()) {
mClearButton.setVisibility(View.VISIBLE);
mMenuButton.setVisibility(View.INVISIBLE);
} else {
mClearButton.setVisibility(View.INVISIBLE);
mMenuButton.setVisibility(View.VISIBLE);
}
}
public void switchToDesktop(@NonNull LauncherState.Desktop mode) {
// get current mode
@Nullable
LauncherState.Desktop currentMode = TBApplication.state().getDesktop();
Log.d(TAG, "desktop changed " + currentMode + " -> " + mode);
if (mode.equals(currentMode)) {
// no change, maybe refresh?
if (TBApplication.state().isResultListVisible() && mResultAdapter.getItemCount() == 0)
showDesktop(mode);
return;
}
// hide current mode
if (currentMode != null) {
switch (currentMode) {
case SEARCH:
resetTask();
hideSearchBar();
break;
case WIDGET:
hideWidgets();
break;
case EMPTY:
default:
break;
}
}
// show next mode
showDesktop(mode);
}
private void showDesktop(LauncherState.Desktop mode) {
if (TBApplication.activityInvalid(mTBLauncherActivity)) {
Log.e(TAG, "[activityInvalid] showDesktop " + mode);
return;
}
TBApplication.state().setDesktop(mode);
switch (mode) {
case SEARCH:
// show the SearchBar
showSearchBar();
// hide/show result list
final String openResult = PrefCache.modeSearchOpenResult(mPref);
if ("none".equals(openResult)) {
// hide result
hideResultList(false);
} else {
// try to execute the action
postDelayedRunnableOnce(() ->
TBApplication.dataHandler(getContext()).runAfterLoadOver(() -> {
LauncherState state = TBApplication.state();
if (!state.isResultListVisible() && state.getDesktop() == LauncherState.Desktop.SEARCH)
executeAction(openResult, "dm-search-open-result");
}),
mLauncherButton,
KEYBOARD_ANIMATION_DELAY);
}
// hide/show the QuickList
TBApplication.quickList(getContext()).updateVisibility();
// enable/disable fullscreen (status and navigation bar)
if (TBApplication.state().isKeyboardHidden()
&& PrefCache.modeSearchFullscreen(mPref))
enableFullscreen(UI_ANIMATION_DELAY);
else
disableFullscreen();
break;
case WIDGET:
// show widgets
showWidgets();
// hide/show the QuickList
TBApplication.quickList(getContext()).updateVisibility();
// enable/disable fullscreen (status and navigation bar)
if (PrefCache.modeWidgetFullscreen(mPref))
enableFullscreen(UI_ANIMATION_DELAY);
else
disableFullscreen();
break;
case EMPTY:
default:
// hide/show the QuickList
TBApplication.quickList(getContext()).updateVisibility();
// enable/disable fullscreen (status and navigation bar)
if (PrefCache.modeEmptyFullscreen(mPref))
enableFullscreen(UI_ANIMATION_DELAY);
else
disableFullscreen();
break;
}
}
/**
* Hide status and notification bar
*
* @param startDelay milliseconds of delay
*/
private void enableFullscreen(int startDelay) {
boolean animate = !SystemUiVisibility.isFullscreenSet(mDecorView) || TBApplication.state().isNotificationBarVisible();
Log.i(TAG, "enableFullscreen delay=" + startDelay + " anim=" + animate);
// Schedule a runnable to remove the status and navigation bar after a delay
removeCallback(mShowPart2Runnable);
postDelayedCallbackOnce(mHidePart2Runnable, startDelay);
// hide notification background
final int statusHeight = UISizes.getStatusBarSize(getContext());
if (TBApplication.state().getNotificationBarVisibility() != LauncherState.AnimatedVisibility.ANIM_TO_HIDDEN)
mNotificationBackground.animate().cancel();
if (animate) {
mNotificationBackground.animate()
.translationY(-statusHeight)
.setStartDelay(startDelay)
.setDuration(UI_ANIMATION_DURATION)
.setInterpolator(new AccelerateInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
TBApplication.state().setNotificationBar(LauncherState.AnimatedVisibility.ANIM_TO_HIDDEN);
}
@Override
public void onAnimationEnd(Animator animation) {
TBApplication.state().setNotificationBar(LauncherState.AnimatedVisibility.HIDDEN);
}
})
.start();
} else {
TBApplication.state().setNotificationBar(LauncherState.AnimatedVisibility.HIDDEN);
mNotificationBackground.setTranslationY(-statusHeight);
}
}
/**
* Show status and notification bar
*/
private void disableFullscreen() {
boolean animate = SystemUiVisibility.isFullscreenSet(mDecorView) || !TBApplication.state().isNotificationBarVisible();
// Schedule a runnable to display UI elements after a delay
removeCallback(mHidePart2Runnable);
postDelayedCallbackOnce(mShowPart2Runnable, UI_ANIMATION_DELAY);
// show notification background
final int statusHeight = UISizes.getStatusBarSize(getContext());
if (!TBApplication.state().isNotificationBarVisible())
mNotificationBackground.setTranslationY(-statusHeight);
if (TBApplication.state().getNotificationBarVisibility() != LauncherState.AnimatedVisibility.ANIM_TO_VISIBLE)
mNotificationBackground.animate().cancel();
if (animate) {
mNotificationBackground.animate()
.translationY(0f)
.setStartDelay(0)
.setDuration(UI_ANIMATION_DURATION)
.setInterpolator(new LinearInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
TBApplication.state().setNotificationBar(LauncherState.AnimatedVisibility.ANIM_TO_VISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
TBApplication.state().setNotificationBar(LauncherState.AnimatedVisibility.VISIBLE);
}
})
.start();
} else {
TBApplication.state().setNotificationBar(LauncherState.AnimatedVisibility.VISIBLE);
mNotificationBackground.setTranslationY(0f);
}
}
private void showSearchBar() {
mSearchEditText.setEnabled(true);
setSearchHint();
UITheme.applySearchBarTextShadow(mSearchEditText);
if (TBApplication.state().getSearchBarVisibility() != LauncherState.AnimatedVisibility.ANIM_TO_VISIBLE)
mSearchBarContainer.animate().cancel();
mSearchBarContainer.setVisibility(View.VISIBLE);
mSearchBarContainer.animate()
.setStartDelay(0)
.alpha(1f)
.translationY(0f)
.setDuration(UI_ANIMATION_DURATION)
.setInterpolator(new DecelerateInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
TBApplication.state().setSearchBar(LauncherState.AnimatedVisibility.ANIM_TO_VISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
LauncherState state = TBApplication.state();
state.setSearchBar(LauncherState.AnimatedVisibility.VISIBLE);
if (PrefCache.linkKeyboardAndSearchBar(mPref))
showKeyboard();
else {
// sync keyboard state
state.syncKeyboardVisibility(mSearchEditText);
}
}
})
.start();
mSearchEditText.requestFocus();
}
private void hideWidgets() {
TBApplication.state().setWidgetScreen(LauncherState.AnimatedVisibility.HIDDEN);
mWidgetContainer.setVisibility(View.GONE);
}
private void hideSearchBar() {
boolean animate = !TBApplication.state().isSearchBarVisible();
hideSearchBar(animate);
}
private void hideSearchBar(boolean animate) {
clearSearchText();
clearAdapter();
if (mSearchBarContainer.getVisibility() == View.VISIBLE) {
final float translationY;
if (PrefCache.searchBarAtBottom(mPref))
translationY = mSearchBarContainer.getHeight() * 2f;
else
translationY = mSearchBarContainer.getHeight() * -2f;
if (TBApplication.state().getSearchBarVisibility() != LauncherState.AnimatedVisibility.ANIM_TO_HIDDEN)
mSearchBarContainer.animate().cancel();
if (animate) {
mSearchBarContainer.setTranslationY(0f);
mSearchBarContainer.animate()
.alpha(0f)
.translationY(translationY)
.setDuration(UI_ANIMATION_DURATION)
.setInterpolator(new AccelerateInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
TBApplication.state().setSearchBar(LauncherState.AnimatedVisibility.ANIM_TO_HIDDEN);
}
@Override
public void onAnimationEnd(Animator animation) {
TBApplication.state().setSearchBar(LauncherState.AnimatedVisibility.HIDDEN);
mSearchBarContainer.setVisibility(View.GONE);
}
})
.start();
} else {
TBApplication.state().setSearchBar(LauncherState.AnimatedVisibility.HIDDEN);
mSearchBarContainer.setAlpha(0f);
mSearchBarContainer.setTranslationY(translationY);
mSearchBarContainer.setVisibility(View.GONE);
}
} else {
Log.d(TAG, "mSearchBarContainer not VISIBLE, setting state to HIDDEN");
TBApplication.state().setResultList(LauncherState.AnimatedVisibility.HIDDEN);
}
if (PrefCache.linkKeyboardAndSearchBar(mPref))
hideKeyboard();
// disabling mSearchEditText will most probably also close the keyboard
mSearchEditText.setEnabled(false);
}
private void showWidgets() {
boolean animate = !TBApplication.state().isWidgetScreenVisible();
showWidgets(animate);
}
private void showWidgets(boolean animate) {
if (TBApplication.state().getWidgetScreenVisibility() != LauncherState.AnimatedVisibility.ANIM_TO_VISIBLE)
mSearchBarContainer.animate().cancel();
mWidgetContainer.setVisibility(View.VISIBLE);
if (animate) {
mWidgetContainer.setAlpha(0f);
mWidgetContainer.animate()
.setStartDelay(UI_ANIMATION_DURATION)
.alpha(1f)
.setDuration(UI_ANIMATION_DURATION)
.setInterpolator(new DecelerateInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
TBApplication.state().setWidgetScreen(LauncherState.AnimatedVisibility.ANIM_TO_VISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
TBApplication.state().setWidgetScreen(LauncherState.AnimatedVisibility.VISIBLE);
}
})
.start();
} else {
mWidgetContainer.animate().cancel();
TBApplication.state().setWidgetScreen(LauncherState.AnimatedVisibility.VISIBLE);
mWidgetContainer.setAlpha(1f);
}
hideResultList(animate);
}
public void showKeyboard() {
mKeyboardHandler.mRequestOpen = true;
mKeyboardHandler.showKeyboard();
// UI_ANIMATION_DURATION should be the exact time the full-screen animation ends
postDelayedCallbackOnce(mShowKeyboardRunnable, UI_ANIMATION_DELAY);
}
public void hideKeyboard() {
mKeyboardHandler.mRequestOpen = false;
removeCallback(mShowKeyboardRunnable);
mKeyboardHandler.hideKeyboard();
}
@Override
public void displayLoader(boolean running) {
if (mLauncherButton == null)
return;
Drawable loadingDrawable = mLauncherButton.getDrawable();
if (loadingDrawable instanceof Animatable) {
if (running)
((Animatable) loadingDrawable).start();
else
((Animatable) loadingDrawable).stop();
}
}
@NonNull
@Override
public Context getContext() {
return mTBLauncherActivity;
}
@Override
public void resetTask() {
TBApplication.resetTask(getContext());
}
@Override
public void clearAdapter() {
mResultAdapter.clear();
TBApplication.quickList(getContext()).adapterCleared();
if (TBApplication.state().isResultListVisible())
hideResultList(true);
updateClearButton();
}
public boolean showProviderEntries(@Nullable IProvider> provider) {
return showProviderEntries(provider, null);
}
public boolean showProviderEntries(@Nullable IProvider> provider, @Nullable java.util.Comparator super EntryItem> comparator) {
if (TBApplication.state().getDesktop() != LauncherState.Desktop.SEARCH) {
// TODO: switchToDesktop might show the result list, we may need to prevent this as an optimization
switchToDesktop(LauncherState.Desktop.SEARCH);
clearAdapter();
}
List extends EntryItem> entries = provider != null ? provider.getPojos() : null;
if (entries != null && entries.size() > 0) {
// reset relevance. This is normally done by a Searcher.
for (EntryItem entry : entries)
entry.resetResultInfo();
// // copy list in order to change it
// entries = new ArrayList<>(entries);
// // remove actions and filters from the result list
// for (Iterator extends EntryItem> iterator = entries.iterator(); iterator.hasNext(); ) {
// EntryItem entry = iterator.next();
// if (entry instanceof FilterEntry)
// iterator.remove();
// }
if (comparator != null) {
// copy list in order to change it
entries = new ArrayList<>(entries);
//TODO: do we need this on another thread?
Collections.sort(entries, comparator);
}
updateAdapter(entries, false);
return true;
}
return false;
}
@Override
public void updateAdapter(@NonNull List extends EntryItem> results, boolean isRefresh) {
Log.d(TAG, "updateAdapter " + results.size() + " result(s); isRefresh=" + isRefresh);
if (!isFragmentDialogVisible()) {
LauncherState state = TBApplication.state();
if (!state.isResultListVisible() && state.getDesktop() == LauncherState.Desktop.SEARCH)
showResultList(false);
}
mResultAdapter.updateItems(results);
if (!isRefresh) {
// Make sure the first item is visible when we search
mResultList.scrollToFirstItem();
}
mTBLauncherActivity.quickList.adapterUpdated();
mClearButton.setVisibility(View.VISIBLE);
mMenuButton.setVisibility(View.INVISIBLE);
if (mLaunchMostRelevantResult) {
mLaunchMostRelevantResult = false;
// get any view
View view = mResultList.getLayoutManager() != null ? mResultList.getLayoutManager().getChildAt(0) : null;
final int mostRelevantIdx = mResultList.getAdapterFirstItemIdx();
// try to get view of the most relevant item from adapter
if (mostRelevantIdx >= 0 && mostRelevantIdx < mResultAdapter.getItemCount()) {
RecyclerView.ViewHolder holder = mResultList.findViewHolderForAdapterPosition(mostRelevantIdx);
if (holder != null)
view = holder.itemView;
}
if (view != null)
mResultAdapter.onClick(mostRelevantIdx, view);
}
}
@Override
public void removeResult(@NonNull EntryItem result) {
// Do not reset scroll, we want the remaining items to still be in view
mResultAdapter.removeItem(result);
}
@Override
public void filterResults(String text) {
mResultAdapter.getFilter().filter(text);
}
public void handleRemoveApp(String packageName) {
int count = mResultAdapter.getItemCount();
for (int idx = count - 1; idx >= 0; idx -= 1) {
EntryItem entryItem = mResultAdapter.getItem(idx);
if (entryItem.id.contains(packageName))
removeResult(entryItem);
}
}
public void runSearcher(@NonNull String query, @NonNull Class extends Searcher> searcherClass) {
if (TBApplication.state().getDesktop() != LauncherState.Desktop.SEARCH) {
// TODO: switchToDesktop might show the result list, we may need to prevent this as an optimization
switchToDesktop(LauncherState.Desktop.SEARCH);
clearAdapter();
}
clearSearchText();
Searcher searcher = null;
try {
Constructor extends Searcher> constructor = searcherClass.getConstructor(ISearchActivity.class, String.class);
searcher = constructor.newInstance(this, query);
} catch (ReflectiveOperationException e) {
Log.e(TAG, "new extends Searcher>", e);
}
if (searcher != null)
updateSearchRecords(false, searcher);
}
public void clearSearchText() {
if (mSearchEditText == null)
return;
mSearchEditText.removeTextChangedListener(mSearchTextWatcher);
mSearchEditText.setText("");
mSearchEditText.addTextChangedListener(mSearchTextWatcher);
}
public void clearSearch() {
clearSearchText();
clearAdapter();
updateClearButton();
}
public void refreshSearchRecords() {
if (mResultList != null) {
mResultList.getRecycledViewPool().clear();
}
if (mResultAdapter != null) {
mResultAdapter.setGridLayout(getContext(), isGridLayout());
mResultAdapter.refresh();
}
}
public void refreshSearchRecord(EntryItem entry) {
mResultAdapter.notifyItemChanged(entry);
}
private void showResultList(boolean animate) {
Log.d(TAG, "showResultList (anim " + animate + ")");
if (TBApplication.state().getResultListVisibility() != LauncherState.AnimatedVisibility.ANIM_TO_VISIBLE)
mResultLayout.animate().cancel();
mResultLayout.setVisibility(View.VISIBLE);
if (animate) {
TBApplication.state().setResultList(LauncherState.AnimatedVisibility.ANIM_TO_VISIBLE);
mResultLayout.setAlpha(0f);
mResultLayout.animate()
.alpha(1f)
.setDuration(UI_ANIMATION_DURATION)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
TBApplication.state().setResultList(LauncherState.AnimatedVisibility.VISIBLE);
}
})
.start();
} else {
TBApplication.state().setResultList(LauncherState.AnimatedVisibility.VISIBLE);
mResultLayout.setAlpha(1f);
}
}
private void hideResultList(boolean animate) {
Log.d(TAG, "hideResultList (anim " + animate + ")");
if (TBApplication.state().getResultListVisibility() != LauncherState.AnimatedVisibility.ANIM_TO_HIDDEN)
mResultLayout.animate().cancel();
if (mResultLayout.getVisibility() != View.VISIBLE) {
Log.d(TAG, "mResultLayout not VISIBLE, setting state to HIDDEN");
TBApplication.state().setResultList(LauncherState.AnimatedVisibility.HIDDEN);
return;
}
if (animate) {
TBApplication.state().setResultList(LauncherState.AnimatedVisibility.ANIM_TO_HIDDEN);
mResultLayout.animate()
.alpha(0f)
.setDuration(UI_ANIMATION_DURATION)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
TBApplication.state().setResultList(LauncherState.AnimatedVisibility.HIDDEN);
Log.d(TAG, "mResultLayout set INVISIBLE");
mResultLayout.setVisibility(View.INVISIBLE);
}
})
.start();
} else {
TBApplication.state().setResultList(LauncherState.AnimatedVisibility.HIDDEN);
Log.d(TAG, "mResultLayout set INVISIBLE");
mResultLayout.setVisibility(View.INVISIBLE);
}
}
public void updateSearchRecords() {
Editable searchText = mSearchEditText.getText();
if (searchText.length() > 0) {
String text = searchText.toString();
updateSearchRecords(true, text);
} else {
refreshSearchRecords();
}
}
/**
* This function gets called on query changes.
* It will ask all the providers for data
* This function is not called for non search-related changes! Have a look at onDataSetChanged() if that's what you're looking for :)
*
* @param isRefresh whether the query is refreshing the existing result, or is a completely new query
* @param query the query on which to search
*/
private void updateSearchRecords(boolean isRefresh, @NonNull String query) {
updateSearchRecords(isRefresh, new QuerySearcher(this, query));
}
private void updateSearchRecords(boolean isRefresh, @NonNull Searcher searcher) {
searcher.setRefresh(isRefresh);
resetTask();
dismissPopup();
TBApplication.runTask(getContext(), searcher);
boolean animate = !TBApplication.state().isResultListVisible();
showResultList(animate);
}
public void beforeLaunchOccurred() {
RecycleScrollListener.setListLayoutHeight(mResultList, mResultList.getHeight());
// don't call the keyboard closed event
mKeyboardHandler.mLaunchedApp = true;
hideKeyboard();
}
public void afterLaunchOccurred() {
postDelayedRunnableOnce(() -> {
RecycleScrollListener.setListLayoutHeight(mResultList, ViewGroup.LayoutParams.MATCH_PARENT);
if (PrefCache.clearSearchAfterLaunch(mPref)) {
// We selected an item on the list, now we can cleanup the filter:
if (mSearchEditText.getText().length() > 0) {
mSearchEditText.setText("");
} else if (TBApplication.state().isResultListVisible()) {
clearAdapter();
}
}
if (PrefCache.showWidgetScreenAfterLaunch(mPref)) {
// show widgets when we return to the launcher
switchToDesktop(LauncherState.Desktop.WIDGET);
}
}, mSearchEditText, UI_ANIMATION_DELAY);
}
public void showContextMenu() {
mMenuButton.performClick();
}
public void setListLayout() {
// update adapter draw flags
mResultAdapter.setGridLayout(getContext(), false);
// get layout manager
RecyclerView.LayoutManager layoutManager = mResultList.getLayoutManager();
if (!(layoutManager instanceof CustomRecycleLayoutManager)) {
mResultList.setLayoutManager(layoutManager = new CustomRecycleLayoutManager());
((CustomRecycleLayoutManager) layoutManager).setOverScrollListener(mRecycleScrollListener);
}
CustomRecycleLayoutManager lm = (CustomRecycleLayoutManager) layoutManager;
lm.setBottomToTop(PrefCache.firstAtBottom(mPref));
lm.setColumns(1, false);
}
public void setGridLayout() {
setGridLayout(3);
}
public void setGridLayout(int columnCount) {
// update adapter draw flags
mResultAdapter.setGridLayout(getContext(), true);
// get layout manager
RecyclerView.LayoutManager layoutManager = mResultList.getLayoutManager();
if (!(layoutManager instanceof CustomRecycleLayoutManager)) {
mResultList.setLayoutManager(layoutManager = new CustomRecycleLayoutManager());
((CustomRecycleLayoutManager) layoutManager).setOverScrollListener(mRecycleScrollListener);
}
CustomRecycleLayoutManager lm = (CustomRecycleLayoutManager) layoutManager;
lm.setBottomToTop(PrefCache.firstAtBottom(mPref));
lm.setRightToLeft(PrefCache.rightToLeft(mPref));
lm.setColumns(columnCount, false);
}
public boolean isGridLayout() {
RecyclerView.LayoutManager layoutManager = mResultList.getLayoutManager();
if (layoutManager instanceof CustomRecycleLayoutManager)
return ((CustomRecycleLayoutManager) layoutManager).getColumnCount() > 1;
return false;
}
/**
* Handle the back button press. Returns true if action handled.
*
* @return returns true if action handled
*/
public boolean onBackPressed() {
if (closeFragmentDialog())
return true;
Log.i(TAG, "onBackPressed query=" + mSearchEditText.getText());
mSearchEditText.setText("");
LauncherState.Desktop desktop = TBApplication.state().getDesktop();
if (desktop != null) {
switch (desktop) {
case SEARCH:
executeButtonAction("dm-search-back");
break;
case WIDGET:
executeButtonAction("dm-widget-back");
break;
case EMPTY:
default:
executeButtonAction("dm-empty-back");
break;
}
}
// Calling super.onBackPressed() will quit the launcher, only do this if this is not the user's default home.
// Action not handled (return false) if not the default launcher.
return TBApplication.isDefaultLauncher(mTBLauncherActivity);
}
@NonNull
public static ListPopup getButtonPopup(@NonNull Context ctx, @NonNull String buttonId, @DrawableRes int defaultButtonIcon) {
LinearAdapter adapter = new LinearAdapter();
adapter.add(new LinearAdapter.ItemTitle(ctx, R.string.popup_title_customize));
adapter.add(new LinearAdapter.Item(ctx, R.string.menu_custom_icon));
return ListPopup.create(ctx, adapter).setOnItemClickListener((a, view, pos) -> {
LinearAdapter.MenuItem menuItem = ((LinearAdapter) a).getItem(pos);
@StringRes int id = 0;
if (menuItem instanceof LinearAdapter.Item) {
id = ((LinearAdapter.Item) a.getItem(pos)).stringId;
}
if (id == R.string.menu_custom_icon) {
TBApplication.behaviour(ctx).launchCustomIconDialog(buttonId, defaultButtonIcon, () -> {
// refresh search bar preferences to reload the icon
TBApplication.ui(ctx).refreshSearchBar();
});
}
});
}
@NonNull
public static IconSelectDialog getCustomIconDialog(@NonNull Context ctx, boolean hideResultList) {
IconSelectDialog dialog = new IconSelectDialog();
//openFragmentDialog(dialog, DIALOG_CUSTOM_ICON);
if (hideResultList) {
// If results are visible
if (TBApplication.state().isResultListVisible()) {
final Behaviour behaviour = TBApplication.behaviour(ctx);
behaviour.mResultLayout.setVisibility(View.INVISIBLE);
// OnDismiss: We restore mResultLayout visibility
dialog.setOnDismissListener(dlg -> behaviour.mResultLayout.setVisibility(View.VISIBLE));
}
}
//dialog.show(mTBLauncherActivity.getSupportFragmentManager(), DIALOG_CUSTOM_ICON);
return dialog;
}
public void launchCustomIconDialog(AppEntry appEntry) {
IconSelectDialog dialog = getCustomIconDialog(getContext(), false);
dialog
.putArgString("componentName", appEntry.getUserComponentName())
.putArgLong("customIcon", appEntry.getCustomIcon())
.putArgString("entryName", appEntry.getName());
dialog.setOnConfirmListener(drawable -> {
TBApplication app = TBApplication.getApplication(getContext());
if (drawable == null)
app.iconsHandler().restoreDefaultIcon(appEntry);
else
app.iconsHandler().changeIcon(appEntry, drawable);
// force a result refresh to update the icon in the view
refreshSearchRecord(appEntry);
mTBLauncherActivity.queueDockReload();
});
showDialog(dialog, DIALOG_CUSTOM_ICON);
}
public void launchCustomIconDialog(ShortcutEntry shortcutEntry) {
IconSelectDialog dialog = getCustomIconDialog(getContext(), true);
dialog
.putArgString("packageName", shortcutEntry.packageName)
.putArgString("shortcutData", shortcutEntry.shortcutData)
.putArgString("shortcutId", shortcutEntry.id);
dialog.setOnConfirmListener(drawable -> {
final TBApplication app = TBApplication.getApplication(mTBLauncherActivity);
if (drawable == null)
app.iconsHandler().restoreDefaultIcon(shortcutEntry);
else
app.iconsHandler().changeIcon(shortcutEntry, drawable);
// force a result refresh to update the icon in the view
refreshSearchRecord(shortcutEntry);
mTBLauncherActivity.queueDockReload();
});
showDialog(dialog, DIALOG_CUSTOM_ICON);
}
public void launchCustomIconDialog(@NonNull StaticEntry staticEntry) {
launchCustomIconDialog(staticEntry, null);
}
public void launchCustomIconDialog(@NonNull StaticEntry staticEntry, @Nullable Runnable afterConfirmation) {
IconSelectDialog dialog = getCustomIconDialog(getContext(), true);
dialog.putArgString("entryId", staticEntry.id);
dialog.setOnConfirmListener(drawable -> {
final TBApplication app = TBApplication.getApplication(mTBLauncherActivity);
if (drawable == null)
app.iconsHandler().restoreDefaultIcon(staticEntry);
else
app.iconsHandler().changeIcon(staticEntry, drawable);
// force a result refresh to update the icon in the view
refreshSearchRecord(staticEntry);
mTBLauncherActivity.queueDockReload();
if (afterConfirmation != null)
afterConfirmation.run();
});
showDialog(dialog, DIALOG_CUSTOM_ICON);
}
public void launchCustomIconDialog(@NonNull SearchEntry searchEntry, @Nullable Runnable afterConfirmation) {
IconSelectDialog dialog = getCustomIconDialog(getContext(), true);
dialog
.putArgString("searchEntryId", searchEntry.id)
.putArgString("searchName", searchEntry.getName());
dialog.setOnConfirmListener(drawable -> {
final TBApplication app = TBApplication.getApplication(mTBLauncherActivity);
if (drawable == null)
app.iconsHandler().restoreDefaultIcon(searchEntry);
else
app.iconsHandler().changeIcon(searchEntry, drawable);
// force a result refresh to update the icon in the view
refreshSearchRecord(searchEntry);
mTBLauncherActivity.queueDockReload();
if (afterConfirmation != null)
afterConfirmation.run();
});
showDialog(dialog, DIALOG_CUSTOM_ICON);
}
/**
* Change the icon for the "Dial" contact
*
* @param dialEntry entry that currently holds the "Dial" icon
*/
public void launchCustomIconDialog(@NonNull DialContactEntry dialEntry) {
IconSelectDialog dialog = getCustomIconDialog(getContext(), true);
dialog
.putArgString("contactEntryId", dialEntry.id)
.putArgString("contactName", dialEntry.getName());
dialog.setOnConfirmListener(drawable -> {
final TBApplication app = TBApplication.getApplication(getContext());
if (drawable == null)
app.iconsHandler().restoreDefaultIcon(dialEntry);
else
app.iconsHandler().changeIcon(dialEntry, drawable);
// force a result refresh to update the icon in the view
refreshSearchRecord(dialEntry);
mTBLauncherActivity.queueDockReload();
});
showDialog(dialog, DIALOG_CUSTOM_ICON);
}
public void launchCustomIconDialog(@NonNull String buttonId, int defaultButtonIcon, @Nullable Runnable afterConfirmation) {
IconSelectDialog dialog = getCustomIconDialog(getContext(), true);
dialog
.putArgString("buttonId", buttonId)
.putArgInt("defaultIcon", defaultButtonIcon);
dialog.setOnConfirmListener(drawable -> {
var iconsHandler = TBApplication.iconsHandler(getContext());
if (drawable == null)
iconsHandler.restoreDefaultIcon(buttonId);
else
iconsHandler.changeIcon(buttonId, drawable);
if (afterConfirmation != null)
afterConfirmation.run();
});
showDialog(dialog, DIALOG_CUSTOM_ICON);
}
public void launchEditTagsDialog(EntryWithTags entry) {
EditTagsDialog dialog = new EditTagsDialog();
openFragmentDialog(dialog, DIALOG_EDIT_TAGS);
// set args
{
Bundle args = new Bundle();
args.putString("entryId", entry.id);
args.putString("entryName", entry.getName());
dialog.setArguments(args);
}
dialog.setOnConfirmListener(newTags -> {
TBApplication.tagsHandler(getContext()).setTags(entry, newTags);
refreshSearchRecord(entry);
});
dialog.show(mTBLauncherActivity.getSupportFragmentManager(), DIALOG_EDIT_TAGS);
}
public void launchEditQuickListDialog(Context context) {
showDialog(context, new EditQuickListDialog(), DIALOG_EDIT_QUICK_LIST);
}
public void launchTagsManagerDialog(Context context) {
showDialog(context, new TagsManagerDialog(), DIALOG_TAGS_MANAGER);
}
private boolean isFragmentDialogVisible() {
return mFragmentDialog != null && mFragmentDialog.isVisible();
}
/**
* Keep track of the last dialog. Use context to find a SupportFragmentManager
*
* @param context to get the FragmentActivity from
* @param dialog to open
* @param tag name to keep track of
*/
public static void showDialog(Context context, DialogFragment> dialog, String tag) {
if (TBApplication.activityInvalid(context)) {
Log.e(TAG, "[activityInvalid] showDialog " + tag);
return;
}
TBApplication.behaviour(context).showDialog(dialog, tag);
}
private void showDialog(@NonNull DialogFragment> dialog, @Nullable String tag) {
openFragmentDialog(dialog, tag);
dialog.show(mTBLauncherActivity.getSupportFragmentManager(), tag);
}
private void openFragmentDialog(DialogFragment> dialog, @Nullable String tag) {
closeFragmentDialog(tag);
mFragmentDialog = dialog;
}
public boolean closeFragmentDialog() {
return closeFragmentDialog(null);
}
private boolean closeFragmentDialog(@Nullable String tag) {
if (mFragmentDialog != null && mFragmentDialog.isVisible()) {
if (tag != null && tag.equals(mFragmentDialog.getTag())) {
mFragmentDialog.dismiss();
return true;
} else if (tag == null) {
mFragmentDialog.dismiss();
mFragmentDialog = null;
return true;
}
}
mFragmentDialog = null;
return false;
}
private void registerPopup(ListPopup menu) {
TBApplication.getApplication(getContext()).registerPopup(menu);
}
private boolean dismissPopup() {
return TBApplication.getApplication(getContext()).dismissPopup();
}
public void onResume() {
Log.i(TAG, "onResume");
mKeyboardHandler.mLaunchedApp = false;
LauncherState.Desktop desktop = TBApplication.state().getDesktop();
showDesktop(desktop);
mLauncherTime = null;
if (PrefCache.searchBarHasTimer(mPref)) {
mLauncherTime = mSearchBarContainer.findViewById(R.id.launcherTime);
UITheme.applySearchBarTextShadow(mLauncherTime);
mLauncherTime.post(mUpdateTime);
}
}
public void onNewIntent() {
if (mTBLauncherActivity == null)
return;
LauncherState state = TBApplication.state();
Log.i(TAG, "onNewIntent desktop=" + state.getDesktop());
closeFragmentDialog();
Intent intent = mTBLauncherActivity.getIntent();
if (intent != null) {
final String action = intent.getAction();
if (LauncherApps.ACTION_CONFIRM_PIN_SHORTCUT.equals(action)) {
// Save single shortcut via a pin request
ShortcutUtil.addShortcut(mTBLauncherActivity, intent);
return;
}
// Pasting shared text from Sharesheet via intent-filter into kiss search bar
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(intent.getType())) {
String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
// making sure the shared text is not an empty string
if (sharedText != null && sharedText.trim().length() > 0) {
mSearchEditText.setText(sharedText);
return;
} else {
//Toast.makeText(this, R.string.shared_text_empty, Toast.LENGTH_SHORT).show();
}
}
}
executeButtonAction("button-home");
}
public void onWindowFocusChanged(boolean hasFocus) {
Log.i(TAG, "onWindowFocusChanged " + hasFocus);
LauncherState state = TBApplication.state();
if (hasFocus) {
if (mKeyboardHandler.mLaunchedApp) {
Log.d(TAG, "skip focus change; LaunchedApp = true");
} else {
if (state.getDesktop() == LauncherState.Desktop.SEARCH) {
if (state.isSearchBarVisible() && PrefCache.linkKeyboardAndSearchBar(mPref)) {
Log.d(TAG, "SearchBarVisible and linkKeyboardAndSearchBar");
showKeyboard();
} else {
//TODO: find why keyboard gets hidden after onWindowFocusChanged
Log.d(TAG, "state().isKeyboardHidden=" + TBApplication.state().isKeyboardHidden() + " mRequestOpen=" + mKeyboardHandler.mRequestOpen);
if (mKeyboardHandler.mRequestOpen) {
showKeyboard();
}
}
if (TBApplication.state().isResultListVisible() && mResultAdapter.getItemCount() == 0)
showDesktop(TBApplication.state().getDesktop());
else if (TBApplication.state().isResultListVisible()) {
showResultList(false);
} else
hideResultList(true);
} else {
if (state.getDesktop() == LauncherState.Desktop.WIDGET) {
hideKeyboard();
}
}
}
}
}
public static void setActivityOrientation(@NonNull Activity act, @NonNull SharedPreferences pref) {
if (pref.getBoolean("lock-portrait", true)) {
if (pref.getBoolean("sensor-orientation", true))
act.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
else
act.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT);
} else {
if (pref.getBoolean("sensor-orientation", true))
act.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
else
act.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_USER);
}
}
private boolean launchStaticEntry(@NonNull String entryId) {
Context ctx = getContext();
DataHandler dataHandler = TBApplication.dataHandler(ctx);
EntryItem item = dataHandler.getPojo(entryId);
if (item == null) {
item = TagsProvider.newTagEntryCheckId(entryId);
}
if (item instanceof StaticEntry) {
if (TBApplication.state().getDesktop() != LauncherState.Desktop.SEARCH) {
// TODO: switchToDesktop might show the result list, we may need to prevent this as an optimization
switchToDesktop(LauncherState.Desktop.SEARCH);
clearAdapter();
}
// make sure the QuickList will not toggle off
TBApplication.quickList(ctx).adapterCleared();
item.doLaunch(mLauncherButton, LAUNCHED_FROM_GESTURE);
return true;
} else {
Toast.makeText(ctx, ctx.getString(R.string.entry_not_found, entryId), Toast.LENGTH_SHORT).show();
}
return false;
}
private boolean launchActionEntry(@NonNull String action) {
return launchStaticEntry(ActionEntry.SCHEME + action);
}
private boolean launchAppEntry(@NonNull String userComponentName) {
Context ctx = getContext();
UserHandleCompat user = UserHandleCompat.fromComponentName(ctx, userComponentName);
ComponentName component = UserHandleCompat.unflattenComponentName(userComponentName);
String appId = AppEntry.generateAppId(component, user);
EntryItem item = TBApplication.dataHandler(ctx).getPojo(appId);
if (item instanceof AppEntry) {
ResultHelper.launch(mLauncherButton, item, LAUNCHED_FROM_GESTURE);
return true;
}
Toast.makeText(ctx, ctx.getString(R.string.application_not_found, appId), Toast.LENGTH_SHORT).show();
return false;
}
private boolean launchEntryById(@NonNull String entryId) {
Context ctx = getContext();
DataHandler dataHandler = TBApplication.dataHandler(ctx);
EntryItem item = dataHandler.getPojo(entryId);
if (item == null) {
Toast.makeText(ctx, ctx.getString(R.string.entry_not_found, entryId), Toast.LENGTH_SHORT).show();
return false;
}
item.doLaunch(mLauncherButton, LAUNCHED_FROM_GESTURE);
return true;
}
private void executeButtonAction(@Nullable String button) {
if (mPref != null)
executeAction(mPref.getString(button, null), button);
}
private boolean executeGestureAction(@Nullable String gesture) {
if (mPref != null)
return executeAction(mPref.getString(gesture, null), gesture);
return false;
}
private boolean executeAction(@Nullable String action, @Nullable String source) {
if (action == null)
return false;
if (TBApplication.activityInvalid(mTBLauncherActivity)) {
Log.e(TAG, "[activityInvalid] executeAction " + action);
// only do stuff if we are the current activity
return false;
}
Log.d(TAG, "executeAction( action=" + action + " source=" + source + " )");
switch (action) {
case "lockScreen":
if (DeviceAdmin.isAdminActive(mTBLauncherActivity)) {
DeviceAdmin.lockScreen(mTBLauncherActivity);
} else {
Toast.makeText(getContext(), R.string.device_admin_required, Toast.LENGTH_SHORT).show();
}
return true;
case "expandNotificationsPanel":
Utilities.expandNotificationsPanel(mTBLauncherActivity);
return true;
case "expandSettingsPanel":
Utilities.expandSettingsPanel(mTBLauncherActivity);
return true;
case "showSearchBar":
switchToDesktop(LauncherState.Desktop.SEARCH);
return true;
case "showSearchBarAndKeyboard":
switchToDesktop(LauncherState.Desktop.SEARCH);
showKeyboard();
return true;
case "showWidgets":
switchToDesktop(LauncherState.Desktop.WIDGET);
return true;
case "showWidgetsCenter":
switchToDesktop(LauncherState.Desktop.WIDGET);
mTBLauncherActivity.liveWallpaper.resetPosition();
return true;
case "showEmpty":
switchToDesktop(LauncherState.Desktop.EMPTY);
return true;
case "toggleSearchAndWidget":
if (TBApplication.state().getDesktop() == LauncherState.Desktop.SEARCH)
switchToDesktop(LauncherState.Desktop.WIDGET);
else
switchToDesktop(LauncherState.Desktop.SEARCH);
return true;
case "toggleSearchWidgetEmpty": {
final LauncherState.Desktop desktop = TBApplication.state().getDesktop();
if (desktop == LauncherState.Desktop.SEARCH)
switchToDesktop(LauncherState.Desktop.WIDGET);
else if (desktop == LauncherState.Desktop.WIDGET)
switchToDesktop(LauncherState.Desktop.EMPTY);
else
switchToDesktop(LauncherState.Desktop.SEARCH);
return true;
}
case "reloadProviders":
TBApplication.dataHandler(getContext()).reloadProviders();
return true;
case "showAllAppsAZ":
return launchActionEntry("show/apps/byName");
case "toggleGrid":
return launchActionEntry("toggle/grid");
case "showAllAppsZA":
return launchActionEntry("show/apps/byNameReversed");
case "showContactsAZ":
return launchActionEntry("show/contacts/byName");
case "showContactsZA":
return launchActionEntry("show/contacts/byNameReversed");
case "showShortcutsAZ":
return launchActionEntry("show/shortcuts/byName");
case "showShortcutsZA":
return launchActionEntry("show/shortcuts/byNameReversed");
case "showFavorites":
return launchActionEntry("show/favorites/byName");
case "showHistoryByRecency":
return launchActionEntry("show/history/recency");
case "showHistoryByFrequency":
return launchActionEntry("show/history/frequency");
case "showHistoryByFrecency":
return launchActionEntry("show/history/frecency");
case "showHistoryByAdaptive":
return launchActionEntry("show/history/adaptive");
case "showUntagged":
return launchActionEntry("show/untagged");
case "showTagsList":
return launchActionEntry("show/tags/list");
case "showTagsListReversed":
return launchActionEntry("show/tags/listReversed");
case "showTagsMenu": {
View anchor = null;
if ("button-launcher".equals(source))
anchor = mLauncherButton;
Context ctx = mLauncherButton.getContext();
ListPopup menu = TBApplication.tagsHandler(ctx).getTagsMenu(ctx);
registerPopup(menu);
if (anchor != null)
menu.show(anchor);
else
menu.showCenter(mLauncherButton);
}
return true;
case "runApp": {
String runApp = mPref.getString(source + "-app-to-run", null);
if (runApp != null)
return launchAppEntry(runApp);
break;
}
case "runShortcut": {
String runApp = mPref.getString(source + "-shortcut-to-run", null);
if (runApp != null)
return launchEntryById(runApp);
break;
}
case "showEntry": {
String entryToShow = mPref.getString(source + "-entry-to-show", null);
if (entryToShow != null)
return launchStaticEntry(entryToShow);
break;
}
default:
// do nothing
break;
}
return false;
}
public boolean onFlingDownLeft() {
return executeGestureAction("gesture-fling-down-left");
}
public boolean onFlingDownRight() {
return executeGestureAction("gesture-fling-down-right");
}
public boolean onFlingUp() {
return executeGestureAction("gesture-fling-up");
}
public boolean onFlingLeft() {
return executeGestureAction("gesture-fling-left");
}
public boolean onFlingRight() {
return executeGestureAction("gesture-fling-right");
}
public boolean onClick() {
return executeGestureAction("gesture-click");
}
public boolean hasDoubleClick() {
if (mPref == null)
return false;
String action = mPref.getString("gesture-double-click", null);
return action != null && !action.isEmpty() && !action.equals("none");
}
public boolean onDoubleClick() {
return executeGestureAction("gesture-double-click");
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/CustomizeUI.java
================================================
package rocks.tbog.tblauncher;
import static rocks.tbog.tblauncher.customicon.ButtonHelper.BTN_ID_LAUNCHER_PILL;
import static rocks.tbog.tblauncher.customicon.ButtonHelper.BTN_ID_LAUNCHER_WHITE;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.RippleDrawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Build;
import android.provider.Settings;
import android.text.InputType;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.EditText;
import android.widget.ImageView;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.motion.widget.MotionLayout;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import androidx.preference.PreferenceManager;
import rocks.tbog.tblauncher.drawable.LoadingDrawable;
import rocks.tbog.tblauncher.result.ResultViewHelper;
import rocks.tbog.tblauncher.ui.RecyclerList;
import rocks.tbog.tblauncher.ui.SearchEditText;
import rocks.tbog.tblauncher.utils.EdgeGlowHelper;
import rocks.tbog.tblauncher.utils.PrefCache;
import rocks.tbog.tblauncher.utils.UIColors;
import rocks.tbog.tblauncher.utils.UISizes;
import rocks.tbog.tblauncher.utils.Utilities;
public class CustomizeUI {
private TBLauncherActivity mTBLauncherActivity;
private SharedPreferences mPref = null;
private ImageView mNotificationBackground;
private ViewGroup mSearchBarContainer;
private SearchEditText mSearchBar;
private ImageView mLauncherButton;
private ImageView mMenuButton;
private ImageView mClearButton;
private WindowInsetsControllerCompat mWindowHelper;
/**
* InputType that behaves as if the consuming IME is a standard-obeying
* soft-keyboard
*
* *Auto Complete* means "we're handling auto-completion ourselves". Then
* we ignore whatever the IME thinks we should display.
*/
private final static int INPUT_TYPE_STANDARD = InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
/**
* InputType that behaves as if the consuming IME is SwiftKey
*
* *Visible Password* fields will break many non-Latin IMEs and may show
* unexpected behaviour in numerous ways. (#454, #517)
*/
private final static int INPUT_TYPE_WORKAROUND = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
| InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
private final View.OnLayoutChangeListener updateResultFadeOut = new UpdateResultFadeOut();
private final MotionTransitionListener mSearchBarTransition = new MotionTransitionListener();
private static final class UpdateResultFadeOut implements View.OnLayoutChangeListener {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
SharedPreferences pref = TBApplication.getApplication(v.getContext()).preferences();
boolean fadeOut = PrefCache.getResultFadeOut(pref);
if (!fadeOut) {
v.removeOnLayoutChangeListener(this);
return;
}
int oldHeight = oldBottom - oldTop;
if (oldHeight != v.getHeight()) {
int backgroundColor = UIColors.getResultListBackground(pref);
if (!setResultListGradientFade(v, backgroundColor))
v.removeOnLayoutChangeListener(this);
}
}
}
public static final class MotionTransitionListener implements MotionLayout.TransitionListener {
private Runnable transitionToEndListener = null;
public enum TransitionType {STARTED, CHANGE, COMPLETED, TRIGGER}
public void setTransitionToEndListener(Runnable listener) {
transitionToEndListener = listener;
}
@Override
public void onTransitionStarted(MotionLayout motionLayout, int startId, int endId) {
// do nothing
}
@Override
public void onTransitionChange(MotionLayout motionLayout, int startId, int endId, float progress) {
// do nothing
}
@Override
public void onTransitionCompleted(MotionLayout motionLayout, int currentId) {
if (transitionToEndListener != null && motionLayout.getEndState() == currentId)
transitionToEndListener.run();
}
@Override
public void onTransitionTrigger(MotionLayout motionLayout, int triggerId, boolean positive, float progress) {
// do nothing
}
}
@SuppressWarnings("TypeParameterUnusedInFormals")
private T findViewById(@IdRes int id) {
return mTBLauncherActivity.findViewById(id);
}
public void onCreateActivity(TBLauncherActivity tbLauncherActivity) {
mTBLauncherActivity = tbLauncherActivity;
mPref = PreferenceManager.getDefaultSharedPreferences(tbLauncherActivity);
mNotificationBackground = findViewById(R.id.notificationBackground);
mSearchBarContainer = findViewById(R.id.searchBarContainer);
mSearchBar = mSearchBarContainer.findViewById(R.id.launcherSearch);
mLauncherButton = mSearchBarContainer.findViewById(R.id.launcherButton);
mMenuButton = mSearchBarContainer.findViewById(R.id.menuButton);
mClearButton = mSearchBarContainer.findViewById(R.id.clearButton);
if (mSearchBarContainer instanceof MotionLayout)
((MotionLayout) mSearchBarContainer).addTransitionListener(mSearchBarTransition);
mWindowHelper = ViewCompat.getWindowInsetsController(mSearchBarContainer);
}
public void onStart() {
refreshSearchBar();
View resultLayout = findViewById(R.id.resultLayout);
setResultListPref(resultLayout, true);
resultLayout.addOnLayoutChangeListener(updateResultFadeOut);
updateResultFadeOut.onLayoutChange(resultLayout,
resultLayout.getLeft(), resultLayout.getTop(), resultLayout.getRight(), resultLayout.getBottom(),
0, 0, 0, 0);
adjustInputType(mSearchBar);
setNotificationBarColor();
setNavigationBarColor();
}
private void setNotificationBarColor() {
int argb = UIColors.getColor(mPref, "notification-bar-argb");
boolean gradient = mPref.getBoolean("notification-bar-gradient", true);
if (gradient) {
int size = UISizes.getStatusBarSize(getContext());
ViewGroup.LayoutParams params = mNotificationBackground.getLayoutParams();
if (params != null) {
params.height = size;
mNotificationBackground.setLayoutParams(params);
}
Utilities.setColorFilterMultiply(mNotificationBackground, argb);
UIColors.setStatusBarColor(mTBLauncherActivity, 0x00000000);
} else {
mNotificationBackground.setVisibility(View.GONE);
UIColors.setStatusBarColor(mTBLauncherActivity, argb);
}
// Notification drawer icon color
mWindowHelper.setAppearanceLightStatusBars(mPref.getBoolean("black-notification-icons", false));
}
private void setNavigationBarColor() {
int argb = UIColors.getColor(mPref, "navigation-bar-argb");
UIColors.setNavigationBarColor(mTBLauncherActivity, argb, UIColors.setAlpha(argb, 0xFF));
// Navigation bar icon color
mWindowHelper.setAppearanceLightNavigationBars(UIColors.isColorLight(argb));
}
public void refreshSearchBar() {
if (PrefCache.getSearchBarLayout(mPref) == R.layout.search_pill)
setSearchPillPref();
else
setSearchBarPref();
}
private void setSearchBarPref() {
final Context ctx = getContext();
final Resources resources = mSearchBarContainer.getResources();
// size
int barHeight = mPref.getInt("search-bar-height", 0);
if (barHeight <= 1)
barHeight = resources.getInteger(R.integer.default_search_bar_height);
barHeight = UISizes.dp2px(ctx, barHeight);
int textSize = mPref.getInt("search-bar-text-size", 0);
if (textSize <= 1)
textSize = resources.getInteger(R.integer.default_size_text);
// layout height and margins
{
ViewGroup.LayoutParams params = mSearchBarContainer.getLayoutParams();
if (params instanceof ViewGroup.MarginLayoutParams) {
params.height = barHeight;
int hMargin = UISizes.getSearchBarMarginHorizontal(ctx);
int vMargin = UISizes.getSearchBarMarginVertical(ctx);
((ViewGroup.MarginLayoutParams)params).setMargins(hMargin, vMargin, hMargin, vMargin);
mSearchBarContainer.setLayoutParams(params);
} else {
throw new IllegalStateException("mSearchBarContainer has the wrong layout params");
}
}
// text size
{
mSearchBar.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize);
}
final int searchBarRipple = UIColors.setAlpha(UIColors.getColor(mPref, "search-bar-ripple-color"), 0xFF);
final int searchIconColor = UIColors.setAlpha(UIColors.getColor(mPref, "search-bar-icon-color"), 0xFF);
final int argbBackground = UIColors.getColor(mPref, "search-bar-argb");
// text color
{
int searchTextCursor = UIColors.getColor(mPref, "search-bar-cursor-argb");
int searchTextHighlight = UIColors.setAlpha(searchTextCursor, 0x7F);
int searchTextColor = UIColors.getSearchTextColor(ctx);
int searchHintColor = UIColors.setAlpha(searchTextColor, 0xBB);
mSearchBar.setTextColor(searchTextColor);
mSearchBar.setHintTextColor(searchHintColor);
// set color for selection background
mSearchBar.setHighlightColor(searchTextHighlight);
Utilities.setTextCursorColor(mSearchBar, searchTextCursor);
Utilities.setTextSelectHandleColor(mSearchBar, searchBarRipple);
}
// icon
ResultViewHelper.setButtonIconAsync(mLauncherButton, BTN_ID_LAUNCHER_WHITE, context -> {
Drawable drawable = new LoadingDrawable();
Utilities.setColorFilterMultiply(drawable, searchIconColor);
return drawable;
});
// icon color
{
Utilities.setColorFilterMultiply(mMenuButton, searchIconColor);
Utilities.setColorFilterMultiply(mClearButton, searchIconColor);
mLauncherButton.setBackground(getSelectorDrawable(mLauncherButton, searchBarRipple, true));
mMenuButton.setBackground(getSelectorDrawable(mMenuButton, searchBarRipple, true));
mClearButton.setBackground(getSelectorDrawable(mClearButton, searchBarRipple, true));
}
// set background
boolean isGradient = mPref.getBoolean("search-bar-gradient", true);
int cornerRadius = UISizes.getSearchBarRadius(ctx);
if (isGradient || cornerRadius > 0) {
GradientDrawable drawable;
if (isGradient) {
final GradientDrawable.Orientation orientation;
if (PrefCache.searchBarAtBottom(mPref))
orientation = GradientDrawable.Orientation.TOP_BOTTOM;
else
orientation = GradientDrawable.Orientation.BOTTOM_TOP;
int alpha = Color.alpha(argbBackground);
int c1 = UIColors.setAlpha(argbBackground, 0);
int c2 = UIColors.setAlpha(argbBackground, alpha * 3 / 4);
int c3 = UIColors.setAlpha(argbBackground, alpha);
drawable = new GradientDrawable(orientation, new int[]{c1, c2, c3});
} else {
drawable = new GradientDrawable();
drawable.setColor(argbBackground);
}
drawable.setCornerRadius(cornerRadius);
mSearchBarContainer.setBackground(drawable);
} else {
mSearchBarContainer.setBackground(new ColorDrawable(argbBackground));
}
}
private void setSearchPillPref() {
final Context ctx = getContext();
final Resources resources = mSearchBarContainer.getResources();
// size
int barHeight = mPref.getInt("search-bar-height", 0);
if (barHeight <= 1)
barHeight = resources.getInteger(R.integer.default_search_bar_height);
barHeight = UISizes.dp2px(ctx, barHeight);
int textSize = mPref.getInt("search-bar-text-size", 0);
if (textSize <= 1)
textSize = resources.getInteger(R.integer.default_size_text);
// layout height and margins
{
ViewGroup.LayoutParams params = mSearchBarContainer.getLayoutParams();
if (params instanceof ViewGroup.MarginLayoutParams) {
params.height = barHeight;
int hMargin = UISizes.getSearchBarMarginHorizontal(ctx);
int vMargin = UISizes.getSearchBarMarginVertical(ctx);
// left must be touching the margin to look good
((ViewGroup.MarginLayoutParams) params).setMargins(0, vMargin, hMargin, vMargin);
mSearchBarContainer.setLayoutParams(params);
} else {
throw new IllegalStateException("mSearchBarContainer has the wrong layout params");
}
}
// text size
{
mSearchBar.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize);
}
final int searchBarRipple = UIColors.setAlpha(UIColors.getColor(mPref, "search-bar-ripple-color"), 0xFF);
final int searchIconColor = UIColors.setAlpha(UIColors.getColor(mPref, "search-bar-icon-color"), 0xFF);
final int argbBackground = UIColors.getColor(mPref, "search-bar-argb");
// text color
{
int searchTextCursor = UIColors.getColor(mPref, "search-bar-cursor-argb");
int searchTextHighlight = UIColors.setAlpha(searchTextCursor, 0x7F);
int searchTextColor = UIColors.getSearchTextColor(ctx);
int searchHintColor = UIColors.setAlpha(searchTextColor, 0xBB);
mSearchBar.setTextColor(searchTextColor);
mSearchBar.setHintTextColor(searchHintColor);
// set color for selection background
mSearchBar.setHighlightColor(searchTextHighlight);
Utilities.setTextCursorColor(mSearchBar, searchTextCursor);
Utilities.setTextSelectHandleColor(mSearchBar, searchBarRipple);
}
// set icon
{
ResultViewHelper.setButtonIconAsync(mLauncherButton, BTN_ID_LAUNCHER_PILL, context -> {
Drawable drawable = ResourcesCompat.getDrawable(context.getResources(), R.drawable.launcher_pill, null);
Utilities.setColorFilterMultiply(drawable, searchIconColor);
return drawable;
});
}
// icon color
{
Utilities.setColorFilterMultiply(mMenuButton, searchIconColor);
Utilities.setColorFilterMultiply(mClearButton, searchIconColor);
mLauncherButton.setBackground(getSelectorDrawable(mLauncherButton, searchBarRipple, true));
mMenuButton.setBackground(getSelectorDrawable(mMenuButton, searchBarRipple, true));
mClearButton.setBackground(getSelectorDrawable(mClearButton, searchBarRipple, true));
}
// set text bar background
boolean isGradient = mPref.getBoolean("search-bar-gradient", true);
if (isGradient) {
GradientDrawable drawable;
final GradientDrawable.Orientation orientation;
orientation = GradientDrawable.Orientation.LEFT_RIGHT;
int alpha = Color.alpha(argbBackground);
int c1 = UIColors.setAlpha(argbBackground, 0);
int c2 = UIColors.setAlpha(argbBackground, alpha * 3 / 4);
int c3 = UIColors.setAlpha(argbBackground, alpha);
drawable = new GradientDrawable(orientation, new int[]{c1, c2, c3});
mSearchBar.setBackground(drawable);
} else {
mSearchBar.setBackground(new ColorDrawable(argbBackground));
}
// set color for the pill background
ImageView bkgBehindButton = mSearchBarContainer.findViewById(R.id.bkgBehindButton);
if (bkgBehindButton != null)
Utilities.setColorFilterMultiply(bkgBehindButton, argbBackground);
// set menu button background
int cornerRadius = UISizes.getSearchBarRadius(ctx);
if (isGradient || cornerRadius > 0) {
GradientDrawable drawable;
if (isGradient) {
final GradientDrawable.Orientation orientation;
orientation = GradientDrawable.Orientation.RIGHT_LEFT;
int alpha = Color.alpha(argbBackground);
int c1 = UIColors.setAlpha(argbBackground, 0);
int c2 = UIColors.setAlpha(argbBackground, alpha * 3 / 4);
int c3 = UIColors.setAlpha(argbBackground, alpha);
drawable = new GradientDrawable(orientation, new int[]{c1, c2, c3});
} else {
drawable = new GradientDrawable();
drawable.setColor(argbBackground);
}
drawable.setCornerRadius(cornerRadius);
mMenuButton.setBackground(drawable);
mClearButton.setBackground(drawable);
} else {
mMenuButton.setBackground(new ColorDrawable(argbBackground));
mClearButton.setBackground(new ColorDrawable(argbBackground));
}
// set margin between the pill and menu button
{
ViewGroup.LayoutParams params = mLauncherButton.getLayoutParams();
if (params instanceof ViewGroup.MarginLayoutParams) {
int hMargin = UISizes.getSearchBarMarginHorizontal(ctx);
// left must be touching the margin to look good
((ViewGroup.MarginLayoutParams) params).setMargins(0, 0, hMargin, 0);
mLauncherButton.setLayoutParams(params);
} else {
throw new IllegalStateException("mMenuButton has the wrong layout params");
}
}
}
public static void setResultListPref(View resultLayout) {
setResultListPref(resultLayout, false);
}
private static boolean setResultListGradientFade(@NonNull View resultLayout, int backgroundColor) {
Drawable bg = resultLayout.getBackground();
if (bg instanceof GradientDrawable) {
GradientDrawable drawable = (GradientDrawable) bg;
drawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
drawable.setOrientation(GradientDrawable.Orientation.TOP_BOTTOM);
int color = backgroundColor & 0x00ffffff;
int alpha = Color.alpha(backgroundColor);
int c1 = UIColors.setAlpha(color, 0);
int c2 = UIColors.setAlpha(color, alpha * 3 / 4);
int c3 = UIColors.setAlpha(color, alpha);
// compute fade percentage of height
float p = UISizes.getResultIconSize(resultLayout.getContext()) * .5f / resultLayout.getHeight();
// if height is too small, fade only on 66% of the height (33% fade in and 33% fade out)
p = Math.min(p, .33f);
return Utilities.setGradientDrawableColors(drawable,
new int[]{c1, c2, c3, c3, c2, c1},
new float[]{0f, p * .5f, p, 1f - p, 1f - (p * .5f), 1f});
}
return false;
}
public static void setResultListPref(View resultLayout, boolean setMargin) {
Context ctx = resultLayout.getContext();
SharedPreferences pref = TBApplication.getApplication(ctx).preferences();
if (setMargin) {
ViewGroup.LayoutParams params = resultLayout.getLayoutParams();
if (params instanceof ViewGroup.MarginLayoutParams) {
final var margin = UISizes.getResultListMargin(ctx);
((ViewGroup.MarginLayoutParams) params).setMargins(margin.left, margin.top, margin.right, margin.bottom);
}
}
boolean fadeOut = PrefCache.getResultFadeOut(pref);
int backgroundColor = UIColors.getResultListBackground(pref);
int cornerRadius = UISizes.getResultListRadius(ctx);
if (cornerRadius > 0 || fadeOut) {
final GradientDrawable drawable = new GradientDrawable();
drawable.setCornerRadius(cornerRadius);
resultLayout.setBackground(drawable);
if (fadeOut)
setResultListGradientFade(resultLayout, backgroundColor);
else
drawable.setColor(backgroundColor);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// clip list content to rounded corners
resultLayout.setClipToOutline(true);
}
} else {
resultLayout.setBackgroundColor(backgroundColor);
}
int overscrollColor = UIColors.getResultListRipple(ctx);
overscrollColor = UIColors.setAlpha(overscrollColor, 0x7F);
if (resultLayout instanceof AbsListView) {
setListViewSelectorPref((AbsListView) resultLayout, true);
setListViewScrollbarPref(resultLayout);
EdgeGlowHelper.setEdgeGlowColor((AbsListView) resultLayout, overscrollColor);
if (setMargin)
setFadingEdge(resultLayout, fadeOut);
} else {
View list = resultLayout.findViewById(R.id.resultList);
if (list instanceof AbsListView) {
setListViewSelectorPref((AbsListView) list, false);
setListViewScrollbarPref(list);
EdgeGlowHelper.setEdgeGlowColor((AbsListView) list, overscrollColor);
} else if (list instanceof RecyclerList) {
setListViewScrollbarPref(list);
EdgeGlowHelper.setEdgeGlowColor((RecyclerList) list, overscrollColor);
}
if (setMargin)
setFadingEdge(list, fadeOut);
}
}
private static void setFadingEdge(@Nullable View view, boolean enabled) {
if (view == null)
return;
if (enabled)
view.setFadingEdgeLength(UISizes.getResultIconSize(view.getContext()));
view.setVerticalFadingEdgeEnabled(enabled);
}
private void adjustInputType(EditText searchEditText) {
int currentInputType = searchEditText.getInputType();
int requiredInputType;
if (isSuggestionsEnabled()) {
requiredInputType = InputType.TYPE_CLASS_TEXT;
} else {
if (isNonCompliantKeyboard()) {
requiredInputType = INPUT_TYPE_WORKAROUND;
} else {
requiredInputType = INPUT_TYPE_STANDARD;
}
}
if (currentInputType != requiredInputType) {
searchEditText.setInputType(requiredInputType);
}
}
public static void setListViewSelectorPref(AbsListView listView, boolean borderless) {
int touchColor = UIColors.getResultListRipple(listView.getContext());
Drawable selector = getSelectorDrawable(listView, touchColor, borderless);
listView.setSelector(selector);
}
public static Drawable getSelectorDrawable(View view, int color, boolean borderless) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Drawable mask = borderless ? null : new ColorDrawable(Color.WHITE);
Drawable content = borderless ? null : view.getBackground();
return new RippleDrawable(ColorStateList.valueOf(color), content, mask);
} else {
ColorDrawable stateColor = new ColorDrawable(color);
StateListDrawable stateListDrawable = new StateListDrawable();
stateListDrawable.addState(new int[]{android.R.attr.state_selected}, stateColor);
stateListDrawable.addState(new int[]{android.R.attr.state_focused}, stateColor);
stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, stateColor);
stateListDrawable.addState(new int[]{}, new ColorDrawable(Color.TRANSPARENT));
stateListDrawable.setEnterFadeDuration(300);
stateListDrawable.setExitFadeDuration(100);
return stateListDrawable;
}
}
public static void setListViewScrollbarPref(View listView) {
int color = UIColors.getResultListRipple(listView.getContext());
setListViewScrollbarPref(listView, UIColors.setAlpha(color, 0x7F));
}
public static void setListViewScrollbarPref(View listView, int color) {
GradientDrawable drawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, new int[]{color, color});
drawable.setCornerRadius(UISizes.dp2px(listView.getContext(), 3));
drawable.setSize(UISizes.dp2px(listView.getContext(), 4), drawable.getIntrinsicHeight());
Utilities.setVerticalScrollbarThumbDrawable(listView, drawable);
}
public Context getContext() {
return mTBLauncherActivity;
}
@NonNull
public static Drawable getPopupBackgroundDrawable(@NonNull Context ctx) {
int border = UISizes.dp2px(ctx, 1);
int radius = UISizes.getPopupCornerRadius(ctx);
GradientDrawable gradient = new GradientDrawable();
gradient.setCornerRadius(radius);
gradient.setStroke(border, UIColors.getPopupBorderColor(ctx));
gradient.setColor(UIColors.getPopupBackgroundColor(ctx));
return gradient;
}
public static Drawable getDialogButtonBarBackgroundDrawable(@NonNull Resources.Theme theme) {
TypedValue typedValue = new TypedValue();
if (theme.resolveAttribute(android.R.attr.buttonBarStyle, typedValue, true)) {
TypedArray a = theme.obtainStyledAttributes(typedValue.resourceId, new int[]{android.R.attr.background});
Drawable background = a.getDrawable(0);
a.recycle();
return background;
}
return null;
}
/**
* Should we force the keyboard not to display suggestions?
* (swiftkey is broken, see https://github.com/Neamar/KISS/issues/44)
* (same for flesky: https://github.com/Neamar/KISS/issues/1263)
*/
private boolean isNonCompliantKeyboard() {
String currentKeyboard = Settings.Secure.getString(mTBLauncherActivity.getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD).toLowerCase();
return currentKeyboard.contains("swiftkey") || currentKeyboard.contains("flesky") || currentKeyboard.endsWith(".latinime");
}
/**
* Should the keyboard autocomplete and suggest options
*/
private boolean isSuggestionsEnabled() {
return mPref.getBoolean("enable-suggestions-keyboard", false);
}
public void expandSearchPill(int duration) {
if (mSearchBarContainer instanceof MotionLayout) {
((MotionLayout) mSearchBarContainer).setTransitionDuration(duration);
((MotionLayout) mSearchBarContainer).transitionToEnd();
}
}
public void collapseSearchPill(int duration) {
if (mSearchBarContainer instanceof MotionLayout) {
((MotionLayout) mSearchBarContainer).setTransitionDuration(duration);
((MotionLayout) mSearchBarContainer).transitionToStart();
}
}
public void setExpandedSearchPillListener(Runnable listener) {
mSearchBarTransition.setTransitionToEndListener(listener);
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/DeviceAdmin.java
================================================
package rocks.tbog.tblauncher;
import android.app.admin.DeviceAdminReceiver;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
public class DeviceAdmin extends DeviceAdminReceiver {
@Override
public void onEnabled(@NonNull Context context, @NonNull Intent intent) {
super.onEnabled(context, intent);
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
pref.edit().putBoolean("device-admin", true).apply();
}
@Override
public void onDisabled(@NonNull Context context, @NonNull Intent intent) {
super.onDisabled(context, intent);
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
pref.edit().putBoolean("device-admin", false).apply();
}
@NonNull
public static ComponentName getAdminComponent(@NonNull Context context) {
return new ComponentName(context, DeviceAdmin.class);
}
public static boolean isAdminActive(@NonNull Context context) {
Object service = context.getSystemService(Context.DEVICE_POLICY_SERVICE);
if (service instanceof DevicePolicyManager) {
DevicePolicyManager dpm = (DevicePolicyManager) service;
return dpm.isAdminActive(getAdminComponent(context));
}
return false;
}
public static void removeActiveAdmin(@NonNull Context context) {
Object service = context.getSystemService(Context.DEVICE_POLICY_SERVICE);
if (service instanceof DevicePolicyManager) {
DevicePolicyManager dpm = (DevicePolicyManager) service;
dpm.removeActiveAdmin(getAdminComponent(context));
}
}
public static void lockScreen(@NonNull Context context) {
Object service = context.getSystemService(Context.DEVICE_POLICY_SERVICE);
if (service instanceof DevicePolicyManager) {
DevicePolicyManager dpm = (DevicePolicyManager) service;
dpm.lockNow();
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/DrawableCache.java
================================================
package rocks.tbog.tblauncher;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.LruCache;
import java.util.Calendar;
import java.util.Collection;
public class DrawableCache {
private static final String TAG = "DrawCache";
private boolean mEnabled = true;
private final LruCache mCache = new LruCache<>(16);
public void setSize(int maxSize) {
mCache.resize(maxSize);
}
public void setCalendar(String cacheId) {
synchronized (mCache) {
DrawableInfo info = mCache.get(cacheId);
if (info != null)
info.setToday();
}
}
@Nullable
public Drawable getCachedDrawable(@NonNull String cacheId) {
synchronized (mCache) {
DrawableInfo info = mCache.get(cacheId);
if (info != null) {
if (info.isToday())
return info.drawable;
mCache.remove(cacheId);
}
}
return null;
}
public void cacheDrawable(@NonNull String cacheId, @Nullable Drawable drawable) {
synchronized (mCache) {
if (drawable == null) {
mCache.remove(cacheId);
return;
}
if (!mEnabled)
return;
DrawableInfo info = new DrawableInfo(drawable);
mCache.put(cacheId, info);
}
}
public void clearCache() {
synchronized (mCache) {
mCache.evictAll();
}
}
public void onPrefChanged(Context ctx, SharedPreferences pref) {
boolean enabled = pref.getBoolean("cache-drawable", true);
if (enabled != mEnabled) {
mEnabled = enabled;
clearCache();
}
boolean halfSize = pref.getBoolean("cache-half-apps", true);
Collection> apps = TBApplication.appsHandler(ctx).getAllApps();
int size = apps.size();
size = size < 16 ? 16 : halfSize ? (size / 2) : (size * 115 / 100);
Log.i(TAG, "Cache size: " + size);
synchronized (mCache) {
mCache.resize(size);
}
}
public static class DrawableInfo {
public final Drawable drawable;
public int dayOfMonth = 0;
public DrawableInfo(Drawable drawable) {
this.drawable = drawable;
}
/**
* Set day for cached drawable. This is a number indicating the day of the month.
* The first day of the month has value 1.
*/
public void setToday() {
dayOfMonth = Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
}
public boolean isToday() {
if (dayOfMonth == 0)
return true;
return dayOfMonth == Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/DummyLauncherActivity.java
================================================
package rocks.tbog.tblauncher;
import android.app.Activity;
public class DummyLauncherActivity extends Activity {
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/EditTagsDialog.java
================================================
package rocks.tbog.tblauncher;
import android.app.Dialog;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import rocks.tbog.tblauncher.entry.EntryItem;
import rocks.tbog.tblauncher.handler.TagsHandler;
import rocks.tbog.tblauncher.ui.DialogFragment;
import rocks.tbog.tblauncher.ui.DialogWrapper;
public class EditTagsDialog extends DialogFragment> {
private static final String TAG = EditTagsDialog.class.getSimpleName();
private final ArraySet mTagList = new ArraySet<>();
private TagsAdapter mAdapter;
private AutoCompleteTextView mNewTag;
@Override
protected int layoutRes() {
return R.layout.dialog_edit_tags;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Context context = requireDialog().getContext();
setupDefaultButtonOkCancel(context);
Bundle args = getArguments() != null ? getArguments() : new Bundle();
// make sure we use the dialog context
LayoutInflater dialogInflater = inflater.cloneInContext(context);
ViewGroup root = (ViewGroup) super.onCreateView(dialogInflater, container, savedInstanceState);
assert root != null;
// make a layout for the entry we are changing
String entryId = args.getString("entryId", "");
TBApplication app = TBApplication.getApplication(context);
EntryItem entry = app.getDataHandler().getPojo(entryId);
ViewGroup wrapper = root.findViewById(R.id.previewWrapper);
if (wrapper == null)
wrapper = root;
if (entry != null) {
int drawFlags = EntryItem.FLAG_DRAW_LIST | EntryItem.FLAG_DRAW_NAME | EntryItem.FLAG_DRAW_ICON;
View entryView = dialogInflater.inflate(entry.getResultLayout(drawFlags), wrapper, false);
entryView.setId(R.id.iconPreview);
wrapper.addView(entryView, 0);
CustomizeUI.setResultListPref(entryView);
}
return root;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Context context = view.getContext();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
view.setClipToOutline(true);
}
Bundle args = getArguments() != null ? getArguments() : new Bundle();
String entryId = args.getString("entryId", "");
String entryName = args.getString("entryName", "");
// show the app we are changing
EntryItem entry = TBApplication.getApplication(context).getDataHandler().getPojo(entryId);
if (entry == null) {
dismiss();
return;
}
int drawFlags = EntryItem.FLAG_DRAW_LIST | EntryItem.FLAG_DRAW_NAME | EntryItem.FLAG_DRAW_ICON;
entry.displayResult(view.findViewById(R.id.iconPreview), drawFlags);
// prepare the grid with all the tags
mAdapter = new TagsAdapter(mTagList);
GridView gridView = view.findViewById(R.id.grid);
gridView.setAdapter(mAdapter);
mAdapter.setOnItemClickListener((adapter, v, position) -> removeTag(adapter.getItem(position)));
// initialize new tag EditView
mNewTag = view.findViewById(R.id.newTag);
mNewTag.addTextChangedListener(new TextWatcher() {
public void afterTextChanged(Editable s) {
// Auto left-trim text.
if (s.length() > 0 && s.charAt(0) == ' ')
s.delete(0, 1);
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
mNewTag.setOnEditorActionListener((v, actionId, event) -> {
if (event == null) {
if (actionId != EditorInfo.IME_ACTION_NONE) {
String tag = mNewTag.getText().toString();
if (tag.isEmpty()) {
onConfirm(mTagList);
dismiss();
return true;
}
addTag(tag);
return true;
}
} else if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
if (event.getAction() == KeyEvent.ACTION_UP) {
String tag = mNewTag.getText().toString();
addTag(tag);
}
return true;
}
return false;
});
// set the auto complete list
{
List allTags = new ArrayList<>(TBApplication.tagsHandler(context).getValidTags());
Collections.sort(allTags);
mNewTag.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, allTags));
}
// initialize add tag button
ImageView addTag = view.findViewById(R.id.addTag);
addTag.setOnClickListener(v ->
{
String tag = mNewTag.getText().toString();
addTag(tag);
});
}
@Override
public void onButtonClick(@NonNull Button button) {
if (button == Button.POSITIVE) {
String tag = mNewTag.getText().toString();
addTag(tag);
onConfirm(mTagList);
}
super.onButtonClick(button);
}
@Override
public void onStart() {
super.onStart();
Dialog dialog = getDialog();
if (dialog instanceof DialogWrapper) {
((DialogWrapper) dialog).setOnWindowFocusChanged((dlg, hasFocus) -> {
if (hasFocus) {
dlg.setOnWindowFocusChanged(null);
showKeyboard(dlg, mNewTag);
}
});
}
}
private static void showKeyboard(@NonNull Dialog dialog, @NonNull TextView textView) {
Log.i(TAG, "Keyboard - SHOW");
textView.requestFocus();
InputMethodManager mgr = (InputMethodManager) dialog.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
assert mgr != null;
mgr.showSoftInput(textView, InputMethodManager.SHOW_IMPLICIT);
}
private void addTag(String tag) {
tag = tag.trim();
if (tag.length() == 0)
return;
mTagList.add(tag);
mAdapter.notifyDataSetChanged();
mNewTag.setText("");
}
private void removeTag(String tag) {
mTagList.remove(tag);
mAdapter.notifyDataSetChanged();
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Context context = getActivity();
assert context != null;
Bundle args = getArguments() != null ? getArguments() : new Bundle();
String entryId = args.getString("entryId", "");
TagsHandler tagsHandler = TBApplication.tagsHandler(context);
mTagList.clear();
mTagList.addAll(tagsHandler.getTags(entryId));
mAdapter.notifyDataSetChanged();
}
static class TagsAdapter extends BaseAdapter {
private final ArraySet mTags;
private OnItemClickListener mOnItemClickListener = null;
public interface OnItemClickListener {
void onItemClick(TagsAdapter adapter, View view, int position);
}
TagsAdapter(@NonNull ArraySet tags) {
mTags = tags;
}
void setOnItemClickListener(OnItemClickListener listener) {
mOnItemClickListener = listener;
}
@Override
public int getCount() {
return mTags.size();
}
@Override
public String getItem(int position) {
return mTags.valueAt(position);
}
@Override
public long getItemId(int position) {
return getItem(position).hashCode();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final View view;
if (convertView == null) {
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.edit_tag_item, parent, false);
} else {
view = convertView;
}
ViewHolder holder = view.getTag() instanceof ViewHolder ? (ViewHolder) view.getTag() : new ViewHolder(view);
String content = getItem(position);
holder.setContent(content);
holder.buttonView.setOnClickListener(v -> {
if (mOnItemClickListener != null)
mOnItemClickListener.onItemClick(TagsAdapter.this, v, position);
});
return view;
}
static class ViewHolder {
TextView textView;
View buttonView;
ViewHolder(View itemView) {
itemView.setTag(this);
textView = itemView.findViewById(android.R.id.text1);
buttonView = itemView.findViewById(android.R.id.button1);
}
public void setContent(CharSequence content) {
textView.setText(content);
}
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/LauncherState.java
================================================
package rocks.tbog.tblauncher;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import rocks.tbog.tblauncher.ui.WindowInsetsHelper;
public class LauncherState {
public enum AnimatedVisibility {
HIDDEN,
ANIM_TO_HIDDEN,
ANIM_TO_VISIBLE,
VISIBLE,
}
public enum Desktop {
SEARCH,
WIDGET,
EMPTY,
}
private AnimatedVisibility quickList = AnimatedVisibility.HIDDEN;
private AnimatedVisibility searchBar = AnimatedVisibility.HIDDEN;
private AnimatedVisibility resultList = AnimatedVisibility.HIDDEN;
private AnimatedVisibility notificationBar = AnimatedVisibility.HIDDEN;
private AnimatedVisibility widgetScreen = AnimatedVisibility.HIDDEN;
private AnimatedVisibility clearScreen = AnimatedVisibility.HIDDEN;
private AnimatedVisibility keyboard = AnimatedVisibility.HIDDEN;
private Desktop desktop = null;
private static boolean isVisible(AnimatedVisibility state) {
return state == AnimatedVisibility.ANIM_TO_VISIBLE ||
state == AnimatedVisibility.VISIBLE;
}
public boolean isQuickListVisible() {
return isVisible(quickList);
}
public boolean isSearchBarVisible() {
return isVisible(searchBar);
}
public boolean isResultListVisible() {
return isVisible(resultList);
}
public boolean isNotificationBarVisible() {
return isVisible(notificationBar);
}
public boolean isWidgetScreenVisible() {
return isVisible(widgetScreen);
}
public boolean isClearScreenVisible() {
return isVisible(clearScreen);
}
public boolean isKeyboardHidden() {
return !isVisible(keyboard);
}
public void syncKeyboardVisibility(View anyView) {
if (WindowInsetsHelper.isKeyboardVisible(anyView))
setKeyboard(AnimatedVisibility.VISIBLE);
else
setKeyboard(AnimatedVisibility.HIDDEN);
}
@Nullable
public Desktop getDesktop() {
return desktop;
}
public void setNotificationBar(@NonNull AnimatedVisibility state) {
notificationBar = state;
}
public void setSearchBar(@NonNull AnimatedVisibility state) {
searchBar = state;
}
public void setResultList(@NonNull AnimatedVisibility state) {
resultList = state;
}
public void setQuickList(@NonNull AnimatedVisibility state) {
quickList = state;
}
public void setWidgetScreen(@NonNull AnimatedVisibility state) {
widgetScreen = state;
}
public void setKeyboard(@NonNull AnimatedVisibility state) {
keyboard = state;
}
public void setDesktop(@NonNull Desktop mode) {
desktop = mode;
}
@NonNull
public AnimatedVisibility getSearchBarVisibility() {
return searchBar;
}
@NonNull
public AnimatedVisibility getResultListVisibility() {
return resultList;
}
@NonNull
public AnimatedVisibility getNotificationBarVisibility() {
return notificationBar;
}
@NonNull
public AnimatedVisibility getWidgetScreenVisibility() { return widgetScreen; }
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/LiveWallpaper.java
================================================
package rocks.tbog.tblauncher;
import android.app.WallpaperManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Build;
import android.util.Log;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowMetrics;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import java.util.Locale;
import rocks.tbog.tblauncher.ui.ListPopup;
import rocks.tbog.tblauncher.utils.GestureDetectorHelper;
import rocks.tbog.tblauncher.utils.UISizes;
public class LiveWallpaper {
private static final String TAG = "LWP";
private static final int FLING_DELTA_ANGLE = 33;
private static final int GD_TOUCH_SLOP_DP = 16;
private TBLauncherActivity mTBLauncherActivity = null;
private WallpaperManager mWallpaperManager;
private final Point mWindowSize = new Point(1, 1);
private View mContentView;
private final PointF mFirstTouchOffset = new PointF();
private final PointF mFirstTouchPos = new PointF();
private final PointF mLastTouchPos = new PointF();
private final PointF mWallpaperOffset = new PointF(.5f, .5f);
private WallpaperSnapAnim mSnapAnimation;
private VelocityTracker mVelocityTracker;
public static int SCREEN_COUNT_HORIZONTAL = Integer.parseInt("3");
public static int SCREEN_COUNT_VERTICAL = Integer.parseInt("1"); // not tested with values != 1
private boolean lwpScrollPages = true;
private boolean lwpTouch = true;
private boolean lwpDrag = false;
private boolean wpDragAnimate = true;
private boolean wpReturnCenter = true;
private boolean wpStickToSides = false;
private GestureDetector gestureDetector = null;
private final GestureDetector.SimpleOnGestureListener onGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public void onLongPress(@NonNull MotionEvent e) {
if (!TBApplication.state().isWidgetScreenVisible())
return;
View view = mTBLauncherActivity.findViewById(R.id.root_layout);
onLongClick(view);
}
@Override
public boolean onFling(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
long deltaTimeMs = (e1 != null) ? (e2.getEventTime() - e1.getEventTime()) : 0;
if (deltaTimeMs > ViewConfiguration.getDoubleTapTimeout())
return false;
View view = mTBLauncherActivity.findViewById(R.id.root_layout);
float xMove = velocityX;
float yMove = velocityY;
if (e1 != null) {
xMove = e1.getRawX() - e2.getRawX();
yMove = e1.getRawY() - e2.getRawY();
}
return LiveWallpaper.this.onFling(view, xMove, yMove, velocityX, velocityY);
}
@Override
public boolean onDoubleTapEvent(@NonNull MotionEvent e) {
if (e.getActionMasked() == MotionEvent.ACTION_UP) {
View view = mTBLauncherActivity.findViewById(R.id.root_layout);
return onDoubleClick(view);
}
return false;
}
@Override
public boolean onSingleTapUp(@NonNull MotionEvent e) {
// if we have a double tap listener, wait for onSingleTapConfirmed
if (mTBLauncherActivity.behaviour.hasDoubleClick())
return true;
View view = mTBLauncherActivity.findViewById(R.id.root_layout);
return onClick(view);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
// if we have both a double tap and click, handle click here
if (mTBLauncherActivity.behaviour.hasDoubleClick()) {
View view = mTBLauncherActivity.findViewById(R.id.root_layout);
return onClick(view);
}
return false;
}
};
LiveWallpaper() {
// TypedValue typedValue = new TypedValue();
// mainActivity.getTheme().resolveAttribute(android.R.attr.windowShowWallpaper, typedValue, true);
// TypedArray a = mainActivity.obtainStyledAttributes(typedValue.resourceId, new int[]{android.R.attr.windowShowWallpaper});
// wallpaperIsVisible = a.getBoolean(0, true);
// a.recycle();
}
@NonNull
public PointF getWallpaperOffset() {
return mWallpaperOffset;
}
@NonNull
public Point getWindowSize() {
return mWindowSize;
}
public void scroll(MotionEvent e1, MotionEvent e2) {
cacheWindowSize();
mFirstTouchPos.set(e1.getRawX(), e1.getRawY());
mLastTouchPos.set(e2.getRawX(), e2.getRawY());
float xMove = (mFirstTouchPos.x - mLastTouchPos.x) / mWindowSize.x;
float yMove = (mFirstTouchPos.y - mLastTouchPos.y) / mWindowSize.y;
float offsetX = mFirstTouchOffset.x + xMove * 1.01f;
float offsetY = mFirstTouchOffset.y + yMove * 1.01f;
updateWallpaperOffset(offsetX, offsetY);
}
private int prefGetInt(@NonNull SharedPreferences prefs, @NonNull String key, int defaultValue) {
String value = prefs.getString(key, null);
if (value != null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException ignored) {
}
}
return defaultValue;
}
public void onCreateActivity(TBLauncherActivity mainActivity) {
mTBLauncherActivity = mainActivity;
// load preferences
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mainActivity);
lwpScrollPages = prefs.getBoolean("lwp-scroll-pages", true);
lwpTouch = prefs.getBoolean("lwp-touch", true);
lwpDrag = prefs.getBoolean("lwp-drag", false);
wpDragAnimate = prefs.getBoolean("wp-drag-animate", false);
wpReturnCenter = prefs.getBoolean("wp-animate-center", true);
wpStickToSides = prefs.getBoolean("wp-animate-sides", false);
SCREEN_COUNT_VERTICAL = prefGetInt(prefs, "lwp-page-count-vertical", SCREEN_COUNT_VERTICAL);
SCREEN_COUNT_HORIZONTAL = prefGetInt(prefs, "lwp-page-count-horizontal", SCREEN_COUNT_HORIZONTAL);
}
mWallpaperManager = (WallpaperManager) mainActivity.getSystemService(Context.WALLPAPER_SERVICE);
assert mWallpaperManager != null;
// set mContentView before we call updateWallpaperOffset
mContentView = mainActivity.findViewById(android.R.id.content);
resetPageCount();
mSnapAnimation = new WallpaperSnapAnim(this);
mVelocityTracker = null;
View root = mainActivity.findViewById(R.id.root_layout);
root.setOnTouchListener(this::onRootTouch);
gestureDetector = new GestureDetector(mainActivity, onGestureListener);
gestureDetector.setIsLongpressEnabled(true);
GestureDetectorHelper.setGestureDetectorTouchSlop(gestureDetector, UISizes.dp2px(mainActivity, GD_TOUCH_SLOP_DP));
}
public void resetPosition() {
resetPageCount();
}
private void resetPageCount() {
Log.i(TAG, "resetPageCount " + SCREEN_COUNT_HORIZONTAL + "x" + SCREEN_COUNT_VERTICAL);
float xStep = (SCREEN_COUNT_HORIZONTAL > 1) ? (1.f / (SCREEN_COUNT_HORIZONTAL - 1)) : 0.f;
float yStep = (SCREEN_COUNT_VERTICAL > 1) ? (1.f / (SCREEN_COUNT_VERTICAL - 1)) : 0.f;
mWallpaperManager.setWallpaperOffsetSteps(xStep, yStep);
if (isPreferenceLWPScrollPages()) {
mTBLauncherActivity.widgetManager.setPageCount(SCREEN_COUNT_HORIZONTAL, SCREEN_COUNT_VERTICAL);
}
int centerScreenX = SCREEN_COUNT_HORIZONTAL / 2;
int centerScreenY = SCREEN_COUNT_VERTICAL / 2;
updateWallpaperOffset(centerScreenX * xStep, centerScreenY * yStep);
}
private static boolean onClick(View view) {
if (!view.isAttachedToWindow())
return false;
return TBApplication.behaviour(view.getContext()).onClick();
}
private static boolean onDoubleClick(View view) {
if (!view.isAttachedToWindow())
return false;
return TBApplication.behaviour(view.getContext()).onDoubleClick();
}
private static int computeAngle(float x, float y) {
return (int) (.5 + Math.toDegrees(Math.atan2(y, x)));
}
private boolean onFling(View view, float xMove, float yMove, float xVel, float yVel) {
if (!view.isAttachedToWindow())
return false;
final Behaviour behaviour = mTBLauncherActivity.behaviour;
final int angle;
// if (-minMovement < xMove && xMove < minMovement && -minMovement < yMove && yMove < minMovement) {
// // too little movement, use velocity
// angle = computeAngle(xVel, yVel);
// } else {
angle = computeAngle(xMove, yMove);
// }
// fling upwards
if ((90 + FLING_DELTA_ANGLE) > angle && angle > (90 - FLING_DELTA_ANGLE)) {
Log.d(TAG, String.format(Locale.US, "Angle=%d - fling upward", angle));
return behaviour.onFlingUp();
}
// fling downwards
else if ((90 + FLING_DELTA_ANGLE) > -angle && -angle > (90 - FLING_DELTA_ANGLE)) {
Log.d(TAG, String.format(Locale.US, "Angle=%d - fling downward", angle));
final int posX = (int) mFirstTouchPos.x;
if (posX < (mWindowSize.x / 2))
return behaviour.onFlingDownLeft();
else
return behaviour.onFlingDownRight();
}
// fling left
else if (FLING_DELTA_ANGLE > angle && angle > -FLING_DELTA_ANGLE) {
Log.d(TAG, String.format(Locale.US, "Angle=%d - fling left", angle));
return behaviour.onFlingLeft();
}
// fling right
else if ((180 - FLING_DELTA_ANGLE) < angle || angle < (-180 + FLING_DELTA_ANGLE)) {
Log.d(TAG, String.format(Locale.US, "Angle=%d - fling right", angle));
return behaviour.onFlingRight();
}
Log.d(TAG, String.format(Locale.US, "Angle=%d - fling direction uncertain", angle));
return false;
}
private void onLongClick(View view) {
if (!view.isAttachedToWindow()) {
return;
}
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
ListPopup menu = mTBLauncherActivity.widgetManager.getConfigPopup(mTBLauncherActivity);
TBApplication.getApplication(mTBLauncherActivity).registerPopup(menu);
int x = (int) (mLastTouchPos.x + .5f);
int y = (int) (mLastTouchPos.y + .5f);
menu.showAtLocation(view, Gravity.START | Gravity.TOP, x, y);
}
private void cacheWindowSize() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowMetrics windowMetrics = mTBLauncherActivity.getWindowManager().getCurrentWindowMetrics();
//Insets insets = windowMetrics.getWindowInsets().getInsetsIgnoringVisibility(WindowInsets.Type.systemBars());
Rect windowBound = windowMetrics.getBounds();
int width = windowBound.width();// - insets.left - insets.right;
int height = windowBound.height();// - insets.top - insets.bottom;
mWindowSize.set(width, height);
} else {
mTBLauncherActivity.getWindowManager()
.getDefaultDisplay()
.getSize(mWindowSize);
}
}
private boolean initializeSnapAnimation() {
return mSnapAnimation.init(mVelocityTracker);
}
boolean onRootTouch(View view, MotionEvent event) {
if (!view.isAttachedToWindow()) {
return false;
}
Log.d(TAG, "onRootTouch\r\n" + event);
int actionMasked = event.getActionMasked();
boolean eventConsumed = false;
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
mFirstTouchPos.set(event.getRawX(), event.getRawY());
mLastTouchPos.set(mFirstTouchPos);
mFirstTouchOffset.set(mWallpaperOffset);
cacheWindowSize();
if (isScrollEnabled()) {
mContentView.clearAnimation();
}
if (mVelocityTracker != null)
mVelocityTracker.recycle();
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(event);
//send touch event to the LWP
if (isPreferenceLWPTouch())
sendTouchEvent(view, event);
eventConsumed = true;
break;
}
case MotionEvent.ACTION_MOVE: {
mLastTouchPos.set(event.getRawX(), event.getRawY());
float xMove = (mFirstTouchPos.x - mLastTouchPos.x) / mWindowSize.x;
float yMove = (mFirstTouchPos.y - mLastTouchPos.y) / mWindowSize.y;
if (mVelocityTracker != null)
mVelocityTracker.addMovement(event);
if (isScrollEnabled()) {
float offsetX = mFirstTouchOffset.x + xMove * 1.01f;
float offsetY = mFirstTouchOffset.y + yMove * 1.01f;
updateWallpaperOffset(offsetX, offsetY);
}
//send move/drag event to the LWP
if (isPreferenceLWPDrag())
sendTouchEvent(view, event);
if (isScrollEnabled())
eventConsumed = true;
break;
}
case MotionEvent.ACTION_UP: {
// was this a click?
float xMove = (mFirstTouchPos.x - mLastTouchPos.x) / mWindowSize.x;
float yMove = (mFirstTouchPos.y - mLastTouchPos.y) / mWindowSize.y;
if (mVelocityTracker == null) {
Log.d(TAG, String.format(Locale.US, "Move=(%.3f, %.3f)", xMove, yMove));
} else {
mVelocityTracker.addMovement(event);
mVelocityTracker.computeCurrentVelocity(1000 / 30); // 1000 provides px per second
float xVel = mVelocityTracker.getXVelocity();// / mWindowSize.x;
float yVel = mVelocityTracker.getYVelocity();// / mWindowSize.y;
Log.d(TAG, String.format(Locale.US, "Velocity=(%.3f, %.3f)\u2248%d\u00b0 Move=(%.3f, %.3f)\u2248%d\u00b0", xVel, yVel, computeAngle(xVel, yVel), xMove, yMove, computeAngle(xMove, yMove)));
// snap position if needed
if (isScrollEnabled() && initializeSnapAnimation())
mContentView.startAnimation(mSnapAnimation);
}
}
// fallthrough
case MotionEvent.ACTION_CANCEL:
if (isScrollEnabled()) {
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
mVelocityTracker.computeCurrentVelocity(1000 / 30); // 1000 provides px per second
if (initializeSnapAnimation())
mContentView.startAnimation(mSnapAnimation);
mVelocityTracker.recycle();
mVelocityTracker = null;
} else {
if (initializeSnapAnimation())
mContentView.startAnimation(mSnapAnimation);
}
eventConsumed = true;
}
break;
}
eventConsumed = gestureDetector.onTouchEvent(event) || eventConsumed;
Log.d(TAG, "onRootTouch event " + (eventConsumed ? "" : "NOT ") + "consumed");
return eventConsumed;
}
public Context getContext() {
return mTBLauncherActivity;
}
public void onPrefChanged(SharedPreferences prefs, String key) {
switch (key) {
case "lwp-scroll-pages":
lwpScrollPages = prefs.getBoolean("lwp-scroll-pages", true);
break;
case "lwp-touch":
lwpTouch = prefs.getBoolean("lwp-touch", true);
break;
case "lwp-drag":
lwpDrag = prefs.getBoolean("lwp-drag", false);
break;
case "wp-drag-animate":
wpDragAnimate = prefs.getBoolean("wp-drag-animate", false);
break;
case "wp-animate-center":
wpReturnCenter = prefs.getBoolean("wp-animate-center", true);
break;
case "wp-animate-sides":
wpStickToSides = prefs.getBoolean("wp-animate-sides", false);
break;
case "lwp-page-count-vertical": {
int count = prefGetInt(prefs, "lwp-page-count-vertical", SCREEN_COUNT_VERTICAL);
if (SCREEN_COUNT_VERTICAL != count) {
SCREEN_COUNT_VERTICAL = count;
resetPageCount();
}
break;
}
case "lwp-page-count-horizontal": {
int count = prefGetInt(prefs, "lwp-page-count-horizontal", SCREEN_COUNT_HORIZONTAL);
if (SCREEN_COUNT_HORIZONTAL != count) {
SCREEN_COUNT_HORIZONTAL = count;
resetPageCount();
}
break;
}
}
}
private boolean isScrollEnabled() {
return lwpScrollPages || wpDragAnimate;
}
private boolean isPreferenceLWPScrollPages() {
return lwpScrollPages;
}
private boolean isPreferenceLWPTouch() {
return lwpTouch;
}
private boolean isPreferenceLWPDrag() {
return lwpDrag;
}
public boolean isPreferenceWPDragAnimate() {
return wpDragAnimate;
}
public boolean isPreferenceWPReturnCenter() {
return wpReturnCenter;
}
public boolean isPreferenceWPStickToSides() {
return wpStickToSides;
}
private android.os.IBinder getWindowToken() {
return mContentView != null && mContentView.isAttachedToWindow() ? mContentView.getWindowToken() : null;
}
public void updateWallpaperOffset(float offsetX, float offsetY) {
offsetX = Math.max(0.f, Math.min(1.f, offsetX));
offsetY = Math.max(0.f, Math.min(1.f, offsetY));
mWallpaperOffset.set(offsetX, offsetY);
if (isPreferenceLWPScrollPages()) {
mTBLauncherActivity.widgetManager.scroll(offsetX, offsetY);
}
if (isPreferenceWPDragAnimate()) {
android.os.IBinder iBinder = getWindowToken();
if (iBinder != null) {
mWallpaperManager.setWallpaperOffsets(iBinder, offsetX, offsetY);
}
}
}
private void sendTouchEvent(int x, int y, int index) {
android.os.IBinder iBinder = getWindowToken();
if (iBinder != null) {
String command = index == 0 ? WallpaperManager.COMMAND_TAP : WallpaperManager.COMMAND_SECONDARY_TAP;
try {
mWallpaperManager.sendWallpaperCommand(iBinder, command, x, y, 0, null);
} catch (Exception e) {
Log.e(TAG, "sendTouchEvent (" + x + "," + y + ") idx=" + index, e);
}
}
}
private void sendTouchEvent(View view, MotionEvent event) {
int pointerCount = event.getPointerCount();
int[] viewOffset = {0, 0};
// this will not account for a rotated view
view.getLocationOnScreen(viewOffset);
// get index of first finger
int pointerIndex = event.findPointerIndex(0);
if (pointerIndex >= 0 && pointerIndex < pointerCount) {
sendTouchEvent((int) event.getX(pointerIndex) + viewOffset[0], (int) event.getY(pointerIndex) + viewOffset[1], pointerIndex);
}
// get index of second finger
pointerIndex = event.findPointerIndex(1);
if (pointerIndex >= 0 && pointerIndex < pointerCount) {
sendTouchEvent((int) event.getX(pointerIndex) + viewOffset[0], (int) event.getY(pointerIndex) + viewOffset[1], pointerIndex);
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/MimeTypeCache.java
================================================
package rocks.tbog.tblauncher;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorDescription;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SyncAdapterType;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.XmlResourceParser;
import android.provider.ContactsContract;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.collection.ArraySet;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import rocks.tbog.tblauncher.utils.MimeTypeUtils;
import rocks.tbog.tblauncher.utils.PackageManagerUtils;
import rocks.tbog.tblauncher.utils.Timer;
import rocks.tbog.tblauncher.utils.Utilities;
public class MimeTypeCache {
private static final String CONTACTS_DATA_KIND = "ContactsDataKind";
private static final String CONTACT_ATTR_MIME_TYPE = "mimeType";
private static final String CONTACT_ATTR_DETAIL_COLUMN = "detailColumn";
private static final String[] METADATA_CONTACTS_NAMES = new String[]{
"android.provider.ALTERNATE_CONTACTS_STRUCTURE",
"android.provider.CONTACTS_STRUCTURE"
};
private static final String TAG = "MTCache";
// Cached componentName
private final Map componentNames = new HashMap<>();
// Cached label
private final Map labels = new HashMap<>();
// Cached detail columns
private Map mDetailColumnsCache = null;
public synchronized void clearCache() {
this.componentNames.clear();
this.labels.clear();
this.mDetailColumnsCache = null;
}
/**
* @param context so we can get the label
* @param mimeType to look for
* @return label for best matching app by mimetype
*/
public String getLabel(Context context, String mimeType) {
if (labels.containsKey(mimeType)) {
return labels.get(mimeType);
}
final Intent intent = MimeTypeUtils.getIntentByMimeType(mimeType, -1, "");
String label = PackageManagerUtils.getLabel(context, intent);
labels.put(mimeType, label);
return label;
}
public ComponentName getComponentName(Context context, String mimeType) {
if (componentNames.containsKey(mimeType)) {
return componentNames.get(mimeType);
}
final Intent intent = MimeTypeUtils.getIntentByMimeType(mimeType, -1, "");
ComponentName componentName = PackageManagerUtils.getComponentName(context, intent);
this.componentNames.put(mimeType, componentName);
return componentName;
}
/**
* @param context to get the Account system service and PackageManager
* @return all mime types and related data columns from contact sync adapters
*/
public Map fetchDetailColumns(Context context) {
synchronized (this) {
if (mDetailColumnsCache != null)
return mDetailColumnsCache;
}
Timer timer = Timer.startNano();
HashMap detailColumns = new HashMap<>();
// add data columns for known mime types
detailColumns.put(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE, ContactsContract.CommonDataKinds.Email.ADDRESS);
detailColumns.put(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, ContactsContract.CommonDataKinds.Phone.NUMBER);
final Set contactSyncableTypes = new HashSet<>();
SyncAdapterType[] syncAdapterTypes = ContentResolver.getSyncAdapterTypes();
for (SyncAdapterType type : syncAdapterTypes) {
if (type.authority.equals(ContactsContract.AUTHORITY)) {
contactSyncableTypes.add(type.accountType);
}
}
AuthenticatorDescription[] authenticatorDescriptions = ((AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE)).getAuthenticatorTypes();
for (AuthenticatorDescription auth : authenticatorDescriptions) {
if (contactSyncableTypes.contains(auth.type)) {
XmlResourceParser parser = loadContactsXml(context, auth.packageName);
if (parser != null) {
try {
while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (CONTACTS_DATA_KIND.equals(parser.getName())) {
String foundMimeType = null;
String foundDetailColumn = null;
int attributeCount = parser.getAttributeCount();
for (int i = 0; i < attributeCount; i++) {
String attr = parser.getAttributeName(i);
String value = parser.getAttributeValue(i);
if (CONTACT_ATTR_MIME_TYPE.equals(attr)) {
foundMimeType = value;
} else if (CONTACT_ATTR_DETAIL_COLUMN.equals(attr)) {
foundDetailColumn = value;
}
}
if (foundMimeType != null) {
detailColumns.put(foundMimeType, foundDetailColumn);
}
}
}
} catch (IOException | XmlPullParserException e) {
Log.w(TAG, "type=" + auth.type + " package=" + auth.packageName, e);
}
}
}
}
Log.i("time", timer + " to fetch detail data columns");
synchronized (this) {
return mDetailColumnsCache = detailColumns;
}
}
/**
* Loads contact description from other sync providers, search for ContactsAccountType or ContactsSource
* detailed description can be found here https://developer.android.com/guide/topics/providers/contacts-provider
*
* @param context to get the PackageManager
* @param packageName AuthenticatorDescription.packageName
* @return XmlResourceParser for contacts.xml, null if nothing found
*/
@SuppressLint("WrongConstant")
public XmlResourceParser loadContactsXml(Context context, String packageName) {
final PackageManager pm = context.getPackageManager();
final Intent intent = new Intent("android.content.SyncAdapter").setPackage(packageName);
final List intentServices = pm.queryIntentServices(intent,
PackageManager.GET_META_DATA | PackageManager.GET_SERVICES);
if (intentServices != null) {
for (final ResolveInfo resolveInfo : intentServices) {
final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
if (serviceInfo == null) {
continue;
}
for (String metadataName : METADATA_CONTACTS_NAMES) {
final XmlResourceParser parser = serviceInfo.loadXmlMetaData(
pm, metadataName);
if (parser != null) {
return parser;
}
}
}
}
return null;
}
/**
* @param context
* @param mimeType
* @return related detail data column for mime type
*/
public String getDetailColumn(Context context, String mimeType) {
Map detailColumns = fetchDetailColumns(context);
return detailColumns.get(mimeType);
}
private static String greatestCommonPrefix(@NonNull String a, @NonNull String b) {
int minLength = Math.min(a.length(), b.length());
for (int i = 0; i < minLength; i++) {
if (a.charAt(i) != b.charAt(i)) {
return a.substring(0, i);
}
}
return a.substring(0, minLength);
}
/**
* Generates unique labels for given mime types, appends mimeType itself if an app supports multiple mime types
*
* @param context
* @param mimeTypes
* @return labels for given mime types
*/
public Map getUniqueLabels(Context context, Set mimeTypes) {
Map uniqueLabels = new HashMap<>(mimeTypes.size());
// get labels for mime types
Map> mappedMimeTypes = new HashMap<>();
for (String mimeType : mimeTypes) {
String label = getLabel(context, mimeType);
Set mimeTypesPerLabel = mappedMimeTypes.get(label);
if (mimeTypesPerLabel == null) {
mimeTypesPerLabel = new ArraySet<>();
mappedMimeTypes.put(label, mimeTypesPerLabel);
}
mimeTypesPerLabel.add(mimeType);
}
int layoutDirection = context.getResources().getConfiguration().getLayoutDirection();
// check supported mime types and make labels unique
for (String mimeType : mimeTypes) {
String label = getLabel(context, mimeType);
Set mimeTypesPerLabel = mappedMimeTypes.get(label);
if (mimeTypesPerLabel != null && mimeTypesPerLabel.size() > 1) {
String prefix = null;
for (String labelMimeType : mimeTypesPerLabel) {
if (labelMimeType != null) {
if (prefix == null) {
prefix = labelMimeType;
} else {
prefix = greatestCommonPrefix(prefix, labelMimeType);
}
}
}
if (prefix != null) {
// assume dot separated words
int pos = prefix.lastIndexOf('.');
if (pos == -1) {
// no dot found, remove whole prefix
pos = prefix.length();
} else {
// remove words before the dot
pos += 1;
}
label = Utilities.appendString(label, " ", "(" + mimeType.substring(pos) + ")", layoutDirection);
} else {
// no prefix !?
label = Utilities.appendString(label, " ", "(" + MimeTypeUtils.getShortMimeType(mimeType) + ")", layoutDirection);
}
}
uniqueLabels.put(mimeType, label);
}
return uniqueLabels;
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/Permission.java
================================================
package rocks.tbog.tblauncher;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.annotation.NonNull;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.ListIterator;
public class Permission {
public static final int PERMISSION_READ_CONTACTS = 0;
public static final int PERMISSION_CALL_PHONE = 1;
public static final int PERMISSION_READ_PHONE_STATE = 2;
private static final String[] permissions = {
Manifest.permission.READ_CONTACTS,
Manifest.permission.CALL_PHONE,
Manifest.permission.READ_PHONE_STATE,
};
// Static weak reference to the linked activity, this is sadly required
// to ensure classes requesting permission can access activity.requestPermission()
private static WeakReference currentActivity = new WeakReference<>(null);
private static ArrayList permissionListeners = null;
public static boolean checkPermission(Context context, int permission) {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || context.checkSelfPermission(permissions[permission]) == PackageManager.PERMISSION_GRANTED;
}
public static void askPermission(int permission, PermissionResultListener listener) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
}
if (listener != null) {
listener.permission = permission;
if (permissionListeners == null) {
permissionListeners = new ArrayList<>();
}
permissionListeners.add(listener);
}
Activity activity = Permission.currentActivity.get();
if (activity != null) {
activity.requestPermissions(new String[]{permissions[permission]}, permission);
}
}
public Permission(Activity activity) {
// Store the latest reference to a MainActivity
currentActivity = new WeakReference<>(activity);
}
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
if (grantResults.length == 0) {
return;
}
if (permissionListeners != null) {
// Iterator allows to remove while iterating
ListIterator it = permissionListeners.listIterator();
PermissionResultListener permissionListener;
while (it.hasNext()) {
permissionListener = it.next();
if (permissionListener.permission == requestCode) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
permissionListener.onGranted();
} else {
permissionListener.onDenied();
}
it.remove();
}
}
}
}
public static class PermissionResultListener {
public int permission = 0;
public void onGranted() {
}
public void onDenied() {
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/PermissionsManager.java
================================================
package rocks.tbog.tblauncher;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
public interface PermissionsManager {
enum PermissionGroup {
Calendar,
Location,
Contacts,
ExternalStorage,
Notifications,
AppShortcuts,
}
void requestPermission(AppCompatActivity context, PermissionGroup permissionGroup);
/**
* Check if this permission is granted right now without receiving further updates
* about the granted state.
* @return true if the given permission group is fully granted
*/
Boolean checkPermissionOnce(PermissionGroup permissionGroup);
void onRequestPermissionsResult(
int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults
);
void onResume();
Boolean hasPermission(PermissionGroup permissionGroup);
/**
* Special function for the Notification listener to report its status.
* May not be called by anything else.
*/
void reportNotificationListenerState(Boolean running);
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/PinShortcutConfirm.java
================================================
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package rocks.tbog.tblauncher;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import rocks.tbog.tblauncher.db.ShortcutRecord;
import rocks.tbog.tblauncher.entry.EntryItem;
import rocks.tbog.tblauncher.entry.ShortcutEntry;
import rocks.tbog.tblauncher.shortcut.ShortcutUtil;
import rocks.tbog.tblauncher.utils.DebugInfo;
import rocks.tbog.tblauncher.utils.Utilities;
@RequiresApi(api = Build.VERSION_CODES.O)
public class PinShortcutConfirm extends AppCompatActivity implements OnClickListener {
private static final String TAG = "ShortcutConfirm";
protected LauncherApps mLauncherApps;
private EditText mShortcutName;
private LauncherApps.PinItemRequest mRequest;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
requestWindowFeature(Window.FEATURE_NO_TITLE);
Window window = getWindow();
if (window != null) {
window.setDimAmount(0.7f);
window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
}
setContentView(R.layout.pin_shortcut_confirm);
mLauncherApps = getSystemService(LauncherApps.class);
mRequest = mLauncherApps.getPinItemRequest(getIntent());
final ShortcutInfo shortcutInfo = mRequest.getShortcutInfo();
if (shortcutInfo == null) {
Log.e(TAG, "No shortcut info provided");
finish();
return;
}
if (prefs.getBoolean("pin-auto-confirm", false)) {
acceptShortcut();
finish();
return;
}
initViews(shortcutInfo);
}
private void initViews(@NonNull ShortcutInfo shortcutInfo) {
// OK button
{
TextView button1 = findViewById(android.R.id.button1);
button1.setOnClickListener(this);
button1.setText(android.R.string.ok);
}
// Cancel button
{
TextView button2 = findViewById(android.R.id.button2);
button2.setOnClickListener(this);
button2.setText(android.R.string.cancel);
}
// Other button
{
TextView button3 = findViewById(android.R.id.button3);
button3.setVisibility(View.GONE);
View spacer = findViewById(R.id.spacer);
if (spacer != null)
spacer.setVisibility(View.GONE);
}
// Label
{
mShortcutName = findViewById(R.id.shortcutName);
String packageName = packageNameHeuristic(this, shortcutInfo);
String appName = ShortcutUtil.getAppNameFromPackageName(this, packageName);
CharSequence label = shortcutInfo.getLongLabel();
if (label == null)
label = shortcutInfo.getShortLabel();
if (label == null)
label = shortcutInfo.getPackage();
if (!appName.isEmpty())
mShortcutName.setText(getString(R.string.shortcut_with_appName, appName, label));
else
mShortcutName.setText(label);
}
// Description
if (DebugInfo.widgetAdd(this)) {
TextView description = findViewById(R.id.shortcutDetails);
ComponentName activity = shortcutInfo.getActivity();
String htmlString = String.format(
"Shortcut details:
" +
"Long label: %s
" +
"Short label: %s
" +
"Activity: %s
" +
"Publisher: %s
" +
"ID: %s",
shortcutInfo.getLongLabel(),
shortcutInfo.getShortLabel(),
activity != null ? activity.flattenToShortString() : null,
shortcutInfo.getPackage(), // publisher app package
shortcutInfo.getId()
);
description.setText(Html.fromHtml(htmlString, Html.FROM_HTML_MODE_COMPACT));
description.setVisibility(View.VISIBLE);
} else {
findViewById(R.id.shortcutDetails).setVisibility(View.GONE);
}
{
View view = findViewById(R.id.image);
TextView nameView = view.findViewById(android.R.id.text1);
nameView.setVisibility(View.GONE);
ImageView icon1 = view.findViewById(android.R.id.icon1);
setIconsAsync(icon1, shortcutInfo, (ctx) -> mLauncherApps.getShortcutIconDrawable(shortcutInfo, 0));
}
{
View view = findViewById(R.id.imageWithBadge);
TextView nameView = view.findViewById(android.R.id.text1);
nameView.setVisibility(View.GONE);
ImageView icon1 = view.findViewById(android.R.id.icon1);
setIconsAsync(icon1, shortcutInfo, (ctx) -> mLauncherApps.getShortcutBadgedIconDrawable(shortcutInfo, 0));
}
}
private static void setIconsAsync(ImageView icon, ShortcutInfo shortcutInfo, Utilities.GetDrawable getIcon) {
new Utilities.AsyncSetDrawable(icon) {
Drawable appDrawable;
@Override
protected Drawable getDrawable(Context context) {
appDrawable = ShortcutEntry.getAppDrawable(context, shortcutInfo.getId(), shortcutInfo.getPackage(), shortcutInfo, true);
return getIcon.getDrawable(context);
}
@Override
protected void onPostExecute(Drawable drawable) {
ImageView icon1 = (ImageView) weakView.get();
super.onPostExecute(drawable);
if (icon1 != null) {
int drawFlags = EntryItem.FLAG_DRAW_ICON | EntryItem.FLAG_DRAW_ICON_BADGE;
ShortcutEntry.setIcons(drawFlags, icon1, drawable, appDrawable);
}
}
}.execute();
}
@NonNull
public static String packageNameHeuristic(@NonNull Context context, @NonNull ShortcutInfo shortcutInfo) {
Intent intent = shortcutInfo.getIntent();
ComponentName activity = intent != null ? intent.getComponent() : null;
String packageName;
if (activity == null) {
// try to parse the ID to get the package name
String id = shortcutInfo.getId();
int schemePos = id.indexOf("://");
int startPos = schemePos >= 0 ? schemePos + 3 : 0;
int endPos = id.indexOf("/", startPos);
if (endPos == -1)
endPos = id.indexOf("#", startPos);
if (endPos == -1)
endPos = id.length();
packageName = id.substring(startPos, endPos);
String appName = ShortcutUtil.getAppNameFromPackageName(context, packageName);
if (appName.isEmpty())
packageName = null;
if (packageName == null) {
if (shortcutInfo.getActivity() != null)
packageName = shortcutInfo.getActivity().getPackageName();
else
packageName = shortcutInfo.getPackage();
}
} else {
packageName = activity.getPackageName();
}
return packageName;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case android.R.id.button1:
acceptShortcut();
finish();
break;
case android.R.id.button2:
finish();
break;
}
}
private void acceptShortcut() {
final ShortcutInfo shortcutInfo = mRequest.getShortcutInfo();
if (shortcutInfo == null) {
Log.e(TAG, "shortcut info is null");
return;
}
final boolean result = mRequest.accept();
Log.i(TAG, "Accept returned: " + result);
ShortcutRecord record = ShortcutUtil.createShortcutRecord(this, shortcutInfo, false);
if (record != null) {
if (mShortcutName.getText().length() > 0)
record.displayName = mShortcutName.getText().toString();
TBApplication.getApplication(this).getDataHandler().addShortcut(record);
}
mRequest = null;
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/SettingsActivity.java
================================================
package rocks.tbog.tblauncher;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.util.Pair;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.collection.ArraySet;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.ListPreference;
import androidx.preference.MultiSelectListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import rocks.tbog.tblauncher.dataprovider.ShortcutsProvider;
import rocks.tbog.tblauncher.dataprovider.TagsProvider;
import rocks.tbog.tblauncher.db.ExportedData;
import rocks.tbog.tblauncher.db.XmlImport;
import rocks.tbog.tblauncher.drawable.SizeWrappedDrawable;
import rocks.tbog.tblauncher.entry.AppEntry;
import rocks.tbog.tblauncher.entry.EntryItem;
import rocks.tbog.tblauncher.entry.StaticEntry;
import rocks.tbog.tblauncher.entry.TagEntry;
import rocks.tbog.tblauncher.handler.IconsHandler;
import rocks.tbog.tblauncher.preference.BaseListPreferenceDialog;
import rocks.tbog.tblauncher.preference.BaseMultiSelectListPreferenceDialog;
import rocks.tbog.tblauncher.preference.ConfirmDialog;
import rocks.tbog.tblauncher.preference.ContentLoadHelper;
import rocks.tbog.tblauncher.preference.CustomDialogPreference;
import rocks.tbog.tblauncher.preference.EditSearchEnginesPreferenceDialog;
import rocks.tbog.tblauncher.preference.EditSearchHintPreferenceDialog;
import rocks.tbog.tblauncher.preference.IconListPreferenceDialog;
import rocks.tbog.tblauncher.preference.MarginDialog;
import rocks.tbog.tblauncher.preference.OrderListPreferenceDialog;
import rocks.tbog.tblauncher.preference.PreferenceColorDialog;
import rocks.tbog.tblauncher.preference.QuickListPreferenceDialog;
import rocks.tbog.tblauncher.preference.ShadowDialog;
import rocks.tbog.tblauncher.preference.SliderDialog;
import rocks.tbog.tblauncher.preference.TagOrderListPreferenceDialog;
import rocks.tbog.tblauncher.ui.dialog.PleaseWaitDialog;
import rocks.tbog.tblauncher.utils.FileUtils;
import rocks.tbog.tblauncher.utils.MimeTypeUtils;
import rocks.tbog.tblauncher.utils.PrefCache;
import rocks.tbog.tblauncher.utils.PrefOrderedListHelper;
import rocks.tbog.tblauncher.utils.SystemUiVisibility;
import rocks.tbog.tblauncher.utils.UIColors;
import rocks.tbog.tblauncher.utils.UISizes;
import rocks.tbog.tblauncher.utils.UITheme;
import rocks.tbog.tblauncher.utils.Utilities;
public class SettingsActivity extends AppCompatActivity implements PreferenceFragmentCompat.OnPreferenceStartScreenCallback/*, PreferenceFragmentCompat.OnPreferenceStartFragmentCallback*/ {
private final static String INTENT_EXTRA_BACK_STACK_TAGS = "backStackTagList";
private final static ArraySet PREF_THAT_REQUIRE_LAYOUT_UPDATE = new ArraySet<>(Arrays.asList(
"result-list-argb", "result-ripple-color", "result-list-radius", "result-list-row-height",
"notification-bar-argb", "notification-bar-gradient", "black-notification-icons",
"navigation-bar-argb",
"search-bar-height", "search-bar-text-size", "search-bar-radius", "search-bar-gradient", "search-bar-at-bottom",
"search-bar-argb", "search-bar-text-color", "search-bar-icon-color",
"search-bar-ripple-color", "search-bar-cursor-argb", "enable-suggestions-keyboard",
"lock-portrait", "sensor-orientation",
"search-bar-layout", "quick-list-position"
));
private final static ArraySet PREF_LISTS_WITH_DEPENDENCY = new ArraySet<>(Arrays.asList(
"gesture-click",
"gesture-double-click",
"gesture-fling-down-left",
"gesture-fling-down-right",
"gesture-fling-up",
"gesture-fling-left",
"gesture-fling-right",
"button-launcher",
"button-home",
"dm-empty-back",
"dm-search-back",
"dm-widget-back",
"dm-search-open-result"
));
private static final int FILE_SELECT_XML_SET = 63;
private static final int FILE_SELECT_XML_OVERWRITE = 62;
private static final int FILE_SELECT_XML_APPEND = 61;
public static final int ENABLE_DEVICE_ADMIN = 60;
private static final String TAG = "SettAct";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
int theme = UITheme.getSettingsTheme(this);
if (theme != UITheme.ID_NULL)
setTheme(theme);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
if (savedInstanceState == null) {
// Create the fragment only when the activity is created for the first time.
// ie. not after orientation changes
Fragment fragment = getSupportFragmentManager().findFragmentByTag(SettingsFragment.FRAGMENT_TAG);
if (fragment == null) {
fragment = new SettingsFragment();
}
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings_container, fragment, SettingsFragment.FRAGMENT_TAG)
.commit();
restoreBackStack();
}
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
private void restoreBackStack() {
Intent intent = getIntent();
if (intent == null)
return;
ArrayList backStackEntryList = intent.getStringArrayListExtra(INTENT_EXTRA_BACK_STACK_TAGS);
if (backStackEntryList != null)
for (String key : backStackEntryList)
if (key != null)
addToBackStack(key);
}
private void addToBackStack(@NonNull String key) {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
SettingsFragment fragment = new SettingsFragment();
Bundle args = new Bundle();
args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, key);
fragment.setArguments(args);
ft.replace(R.id.settings_container, fragment, key);
ft.addToBackStack(key);
ft.commit();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
String[] themeNames = getResources().getStringArray(R.array.settingsThemeEntries);
for (String name : themeNames)
menu.add(name);
return true;
}
@SuppressLint("ApplySharedPref")
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getTitle() != null) {
String itemName = item.getTitle().toString();
String[] themeNames = getResources().getStringArray(R.array.settingsThemeEntries);
String[] themeValues = getResources().getStringArray(R.array.settingsThemeValues);
for (int themeIdx = 0; themeIdx < themeNames.length; themeIdx++) {
String name = themeNames[themeIdx];
if (itemName.equals(name)) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
sharedPreferences.edit().putString("settings-theme", themeValues[themeIdx]).commit();
restart();
return true;
}
}
}
return super.onOptionsItemSelected(item);
}
private void restart() {
// save backstack
FragmentManager fm = getSupportFragmentManager();
int backStackEntryCount = fm.getBackStackEntryCount();
ArrayList backStackTags = null;
if (backStackEntryCount > 0) {
backStackTags = new ArrayList<>(backStackEntryCount);
for (int idx = 0; idx < backStackEntryCount; idx += 1) {
FragmentManager.BackStackEntry entry = fm.getBackStackEntryAt(idx);
String tag = entry.getName();
backStackTags.add(tag);
}
}
// close current activity
finish();
// start new activity
Intent activityIntent = new Intent(this, getClass());
if (backStackTags != null) {
// remember the back stack pages so we can restore them
activityIntent.putStringArrayListExtra(INTENT_EXTRA_BACK_STACK_TAGS, backStackTags);
}
startActivity(activityIntent);
// set transition animation
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
}
@Override
protected void onTitleChanged(CharSequence title, int color) {
super.onTitleChanged(title, color);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
if (color != 0 && !(title instanceof Spannable)) {
SpannableString ss = new SpannableString(title);
ss.setSpan(new ForegroundColorSpan(color), 0, title.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
actionBar.setTitle(ss);
} else {
actionBar.setTitle(title);
}
}
}
@Override
public boolean onSupportNavigateUp() {
if (getSupportFragmentManager().popBackStackImmediate()) {
final int count = getSupportFragmentManager().getBackStackEntryCount();
CharSequence title = null;
if (count > 0) {
String tag = getSupportFragmentManager().getBackStackEntryAt(count - 1).getName();
if (tag != null) {
Fragment fragment = getSupportFragmentManager().findFragmentByTag(SettingsFragment.FRAGMENT_TAG);
if (fragment instanceof SettingsFragment) {
Preference preference = ((SettingsFragment) fragment).findPreference(tag);
if (preference != null)
title = preference.getTitle();
}
}
}
if (title != null)
setTitle(title);
else
setTitle(R.string.menu_popup_launcher_settings);
return true;
}
return super.onSupportNavigateUp();
}
@Override
public boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen preferenceScreen) {
final String key = preferenceScreen.getKey();
addToBackStack(key);
return true;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Log.d(TAG, "onActivityResult request=" + requestCode + " result=" + resultCode);
if (requestCode == ENABLE_DEVICE_ADMIN) {
if (resultCode != RESULT_OK) {
Toast.makeText(this, "Failed!", Toast.LENGTH_SHORT).show();
}
} else if (resultCode == RESULT_OK) {
ExportedData.Method method = null;
switch (requestCode) {
case FILE_SELECT_XML_APPEND:
method = ExportedData.Method.APPEND;
break;
case FILE_SELECT_XML_OVERWRITE:
method = ExportedData.Method.OVERWRITE;
break;
case FILE_SELECT_XML_SET:
method = ExportedData.Method.SET;
break;
}
if (method != null) {
Uri uri = data != null ? data.getData() : null;
File importedFile = FileUtils.copyFile(this, uri, "imported.xml");
if (importedFile != null) {
PleaseWaitDialog dialog = new PleaseWaitDialog();
// set args
{
Bundle args = new Bundle();
//args.putString(PleaseWaitDialog.ARG_TITLE, getString(R.string.import_dialog_title));
args.putString(PleaseWaitDialog.ARG_DESCRIPTION, getString(R.string.import_dialog_description));
dialog.setArguments(args);
}
final ExportedData.Method importMethod = method;
dialog.setWork(() -> {
Activity activity = Utilities.getActivity(dialog.getContext());
if (activity != null) {
if (!XmlImport.settingsXml(activity, importedFile, importMethod)) {
Toast.makeText(activity, R.string.error_fail_import, Toast.LENGTH_LONG).show();
dialog.dismiss();
}
}
dialog.onWorkFinished();
});
dialog.show(getSupportFragmentManager(), "load_imported");
} else {
Toast.makeText(this, R.string.error_fail_import, Toast.LENGTH_LONG).show();
}
}
}
}
public static class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String FRAGMENT_TAG = SettingsFragment.class.getName();
private static final String DIALOG_FRAGMENT_TAG = "androidx.preference.PreferenceFragment.DIALOG";
private static final String TAG = "Settings";
private static Pair AppToRunListContent = null;
private static Pair ShortcutToRunListContent = null;
private static Pair EntryToShowListContent = null;
private static ContentLoadHelper.OrderedMultiSelectListData TagsMenuContent = null;
private static ContentLoadHelper.OrderedMultiSelectListData ResultPopupContent = null;
private static Pair MimeTypeListContent = null;
public SettingsFragment() {
super();
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
if (rootKey != null && rootKey.startsWith("feature-"))
setPreferencesFromResource(R.xml.preference_features, rootKey);
else
setPreferencesFromResource(R.xml.preferences, rootKey);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
removePreference("black-notification-icons");
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
removePreference("pin-auto-confirm");
}
if (!BuildConfig.SHOW_RATE_APP) {
removePreference("rate-app");
}
if (!BuildConfig.SHOW_PRIVACY_POLICY) {
removePreference("privacy-policy");
}
if (!BuildConfig.DEBUG) {
removePreference("crash-app");
}
// set app name and version
{
Preference appVer = findPreference("app-version");
if (appVer != null) {
var version = appVer.getContext().getString(R.string.app_version, BuildConfig.VERSION_NAME);
var appName = appVer.getContext().getText(R.string.app_name);
String appStore;
switch (BuildConfig.FLAVOR) {
case "playstore":
appStore = "Google Play";
break;
case "fdroid":
appStore = "F-Droid";
break;
case "github":
appStore = "GitHub";
break;
default:
throw new IllegalStateException("Undefined flavor");
}
var summary = appVer.getContext().getString(R.string.app_version_summary, appName, appStore);
appVer.setTitle(version);
appVer.setSummary(summary);
// add link to the launcher webpage if app not installed from a store
if (!BuildConfig.SHOW_RATE_APP) {
appVer.setEnabled(true);
}
}
}
final Activity activity = requireActivity();
// set activity title as the preference screen title
activity.setTitle(getPreferenceScreen().getTitle());
ActionBar actionBar = ((SettingsActivity) activity).getSupportActionBar();
if (actionBar != null) {
// we can change the theme from the options menu
removePreference("settings-theme");
}
setupButtonActions(activity);
final Context context = requireContext();
tintPreferenceIcons(getPreferenceScreen(), UIColors.getThemeColor(context, com.google.android.material.R.attr.colorAccent));
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
// quick-list
{
Preference pref = findPreference("quick-list-enabled");
// if we don't have the toggle in this screen we need to apply dependency by hand
if (pref == null) {
// only show the category if we use the quick list
Preference section = findPreference("quick-list-section");
if (section != null)
section.setVisible(sharedPreferences.getBoolean("quick-list-enabled", true));
}
}
onCreateAsyncLoad(context, sharedPreferences, savedInstanceState);
}
private void setupButtonActions(@NonNull Activity activity) {
// import settings
{
Preference pref = findPreference("import-settings-set");
if (pref != null)
pref.setOnPreferenceClickListener(preference -> {
FileUtils.chooseSettingsFile(activity, FILE_SELECT_XML_SET);
return true;
});
pref = findPreference("import-settings-overwrite");
if (pref != null)
pref.setOnPreferenceClickListener(preference -> {
FileUtils.chooseSettingsFile(activity, FILE_SELECT_XML_OVERWRITE);
return true;
});
pref = findPreference("import-settings-append");
if (pref != null)
pref.setOnPreferenceClickListener(preference -> {
FileUtils.chooseSettingsFile(activity, FILE_SELECT_XML_APPEND);
return true;
});
}
}
private void onCreateAsyncLoad(@NonNull Context context, @NonNull SharedPreferences sharedPreferences, @Nullable Bundle savedInstanceState) {
if (savedInstanceState == null) {
initAppToRunLists(context, sharedPreferences);
initShortcutToRunLists(context, sharedPreferences);
initEntryToShowLists(context, sharedPreferences);
initTagsMenuList(context, sharedPreferences);
initResultPopupList(context, sharedPreferences);
initMimeTypes(context);
} else {
synchronized (SettingsFragment.class) {
if (AppToRunListContent == null)
AppToRunListContent = generateAppToRunListContent(context);
if (ShortcutToRunListContent == null)
ShortcutToRunListContent = generateShortcutToRunListContent(context);
if (EntryToShowListContent == null)
EntryToShowListContent = generateEntryToShowListContent(context);
if (TagsMenuContent == null)
TagsMenuContent = ContentLoadHelper.generateTagsMenuContent(context, sharedPreferences);
if (ResultPopupContent == null)
ResultPopupContent = ContentLoadHelper.generateResultPopupContent(context, sharedPreferences);
if (MimeTypeListContent == null)
MimeTypeListContent = generateMimeTypeListContent(context);
for (String gesturePref : PREF_LISTS_WITH_DEPENDENCY) {
updateAppToRunList(sharedPreferences, gesturePref);
updateShortcutToRunList(sharedPreferences, gesturePref);
updateEntryToShowList(sharedPreferences, gesturePref);
}
TagsMenuContent.setMultiListValues(findPreference("tags-menu-list"));
TagsMenuContent.setOrderedListValues(findPreference("tags-menu-order"));
ResultPopupContent.setOrderedListValues(findPreference("result-popup-order"));
ContentLoadHelper.setMultiListValues(findPreference("selected-contact-mime-types"), MimeTypeListContent, null);
}
}
final ListPreference iconsPack = findPreference("icons-pack");
if (iconsPack != null) {
iconsPack.setEnabled(false);
if (savedInstanceState == null) {
// Run asynchronously to open settings fast
Utilities.runAsync(getLifecycle(),
t -> SettingsFragment.this.setListPreferenceIconsPacksData(iconsPack),
t -> iconsPack.setEnabled(true));
} else {
// Run synchronously to ensure preferences can be restored from state
SettingsFragment.this.setListPreferenceIconsPacksData(iconsPack);
iconsPack.setEnabled(true);
}
}
}
private void initAppToRunLists(@NonNull Context context, @NonNull SharedPreferences sharedPreferences) {
final Runnable updateLists = () -> {
for (String gesturePref : PREF_LISTS_WITH_DEPENDENCY)
updateAppToRunList(sharedPreferences, gesturePref);
};
if (AppToRunListContent == null) {
Utilities.runAsync(getLifecycle(), t -> {
Pair content = generateAppToRunListContent(context);
synchronized (SettingsFragment.class) {
if (AppToRunListContent == null)
AppToRunListContent = content;
}
}, t -> updateLists.run());
} else {
updateLists.run();
}
}
private void initShortcutToRunLists(@NonNull Context context, @NonNull SharedPreferences sharedPreferences) {
final Runnable updateLists = () -> {
for (String gesturePref : PREF_LISTS_WITH_DEPENDENCY)
updateShortcutToRunList(sharedPreferences, gesturePref);
};
if (ShortcutToRunListContent == null) {
Utilities.runAsync(getLifecycle(), t -> {
Pair content = generateShortcutToRunListContent(context);
synchronized (SettingsFragment.this) {
if (ShortcutToRunListContent == null)
ShortcutToRunListContent = content;
}
}, t -> updateLists.run());
} else {
updateLists.run();
}
}
private void initEntryToShowLists(@NonNull Context context, @NonNull SharedPreferences sharedPreferences) {
final Runnable updateLists = () -> {
for (String gesturePref : PREF_LISTS_WITH_DEPENDENCY)
updateEntryToShowList(sharedPreferences, gesturePref);
};
if (EntryToShowListContent == null) {
Utilities.runAsync(getLifecycle(), t -> {
Pair content = generateEntryToShowListContent(context);
synchronized (SettingsFragment.class) {
if (EntryToShowListContent == null)
EntryToShowListContent = content;
}
}, t -> updateLists.run());
} else {
updateLists.run();
}
}
private void initTagsMenuList(@NonNull Context context, @NonNull SharedPreferences sharedPreferences) {
final Runnable setTagsMenuValues = () -> {
synchronized (SettingsFragment.class) {
if (TagsMenuContent != null) {
TagsMenuContent.setMultiListValues(findPreference("tags-menu-list"));
TagsMenuContent.setOrderedListValues(findPreference("tags-menu-order"));
}
}
};
if (TagsMenuContent == null) {
Utilities.runAsync(getLifecycle(), t -> {
ContentLoadHelper.OrderedMultiSelectListData content = ContentLoadHelper.generateTagsMenuContent(context, sharedPreferences);
synchronized (SettingsFragment.class) {
if (TagsMenuContent == null) {
TagsMenuContent = content;
}
}
}, t -> setTagsMenuValues.run());
} else {
setTagsMenuValues.run();
}
}
private void initResultPopupList(@NonNull Context context, @NonNull SharedPreferences sharedPreferences) {
final Runnable setResultPopupValues = () -> {
synchronized (SettingsFragment.class) {
if (ResultPopupContent != null)
ResultPopupContent.setOrderedListValues(findPreference("result-popup-order"));
}
};
if (ResultPopupContent == null) {
Utilities.runAsync(getLifecycle(), t -> {
ContentLoadHelper.OrderedMultiSelectListData content = ContentLoadHelper.generateResultPopupContent(context, sharedPreferences);
synchronized (SettingsFragment.class) {
if (ResultPopupContent == null) {
ResultPopupContent = content;
}
}
}, t -> setResultPopupValues.run());
} else {
setResultPopupValues.run();
}
}
private void initMimeTypes(@NonNull Context context) {
// get all supported mime types
final Runnable setMimeTypeValues = () -> {
synchronized (SettingsFragment.class) {
if (MimeTypeListContent != null)
ContentLoadHelper.setMultiListValues(findPreference("selected-contact-mime-types"), MimeTypeListContent, null);
}
};
if (MimeTypeListContent == null) {
Utilities.runAsync(getLifecycle(), t -> {
Pair content = generateMimeTypeListContent(context);
synchronized (SettingsFragment.class) {
if (MimeTypeListContent == null)
MimeTypeListContent = content;
}
}, t -> setMimeTypeValues.run());
} else {
setMimeTypeValues.run();
}
}
private void tintPreferenceIcons(Preference preference, int color) {
Drawable icon = preference.getIcon();
if (icon != null) {
// workaround to set drawable size
{
int size = UISizes.getResultIconSize(preference.getContext());
icon = new SizeWrappedDrawable(icon, size);
}
icon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY));
preference.setIcon(icon);
}
if (preference instanceof PreferenceGroup) {
PreferenceGroup group = ((PreferenceGroup) preference);
for (int i = 0; i < group.getPreferenceCount(); i++) {
tintPreferenceIcons(group.getPreference(i), color);
}
}
}
private void removePreference(String key) {
Preference pref = findPreference(key);
if (pref != null && pref.getParent() != null)
pref.getParent().removePreference(pref);
}
private void setListPreferenceIconsPacksData(ListPreference lp) {
Context context = getContext();
if (context == null)
return;
IconsHandler iph = TBApplication.getApplication(context).iconsHandler();
CharSequence[] entries = new CharSequence[iph.getIconPackNames().size() + 1];
CharSequence[] entryValues = new CharSequence[iph.getIconPackNames().size() + 1];
int i = 0;
entries[0] = this.getString(R.string.icons_pack_default_name);
entryValues[0] = "default";
for (String packageIconsPack : iph.getIconPackNames().keySet()) {
entries[++i] = iph.getIconPackNames().get(packageIconsPack);
entryValues[i] = packageIconsPack;
}
lp.setEntries(entries);
lp.setDefaultValue("default");
lp.setEntryValues(entryValues);
}
@Override
public void onResume() {
super.onResume();
SharedPreferences sharedPreferences = getPreferenceScreen().getSharedPreferences();
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
applyNotificationBarColor(sharedPreferences, requireContext());
}
@Override
public void onPause() {
super.onPause();
getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
}
@Override
public void onDestroy() {
super.onDestroy();
synchronized (SettingsFragment.class) {
AppToRunListContent = null;
ShortcutToRunListContent = null;
EntryToShowListContent = null;
TagsMenuContent = null;
ResultPopupContent = null;
}
}
@Override
public void onDisplayPreferenceDialog(@NonNull Preference preference) {
String key = preference.getKey();
// Try if the preference is one of our custom Preferences
DialogFragment dialogFragment;
if (preference instanceof CustomDialogPreference) {
// Create a new instance of CustomDialog with the key of the related Preference
Log.d(TAG, "onDisplayPreferenceDialog " + key);
switch (key) {
case "quick-list-content":
dialogFragment = QuickListPreferenceDialog.newInstance(key);
break;
case "reset-search-engines":
case "edit-search-engines":
case "add-search-engine":
dialogFragment = EditSearchEnginesPreferenceDialog.newInstance(key);
break;
case "reset-search-hint":
case "edit-search-hint":
case "add-search-hint":
dialogFragment = EditSearchHintPreferenceDialog.newInstance(key);
break;
default:
dialogFragment = null;
}
if (dialogFragment == null) {
@LayoutRes
int dialogLayout = ((CustomDialogPreference) preference).getDialogLayoutResource();
if (dialogLayout == 0) {
if (key.endsWith("-color") || key.endsWith("-argb"))
dialogFragment = PreferenceColorDialog.newInstance(key);
} else if (R.layout.pref_slider == dialogLayout) {
dialogFragment = SliderDialog.newInstance(key);
} else if (R.layout.pref_shadow == dialogLayout) {
dialogFragment = ShadowDialog.newInstance(key);
} else if (R.layout.pref_margin_offset == dialogLayout) {
dialogFragment = MarginDialog.newInstance(key);
} else if (R.layout.pref_confirm == dialogLayout) {
dialogFragment = ConfirmDialog.newInstance(key);
}
}
if (dialogFragment == null)
throw new IllegalArgumentException("CustomDialogPreference \"" + key + "\" has no dialog defined");
} else if (preference instanceof ListPreference) {
switch (key) {
case "adaptive-shape":
case "contacts-shape":
case "shortcut-shape":
case "icons-pack":
dialogFragment = IconListPreferenceDialog.newInstance(key);
break;
default:
dialogFragment = BaseListPreferenceDialog.newInstance(key);
break;
}
} else if (preference instanceof MultiSelectListPreference) {
if ("tags-menu-order".equals(key)) {
dialogFragment = TagOrderListPreferenceDialog.newInstance(key);
} else if ("result-popup-order".equals(key)) {
dialogFragment = OrderListPreferenceDialog.newInstance(key);
} else {
dialogFragment = BaseMultiSelectListPreferenceDialog.newInstance(key);
}
} else {
Log.i(TAG, "Preference \"" + key + "\" has no custom dialog defined");
dialogFragment = null;
}
// If it was one of our custom Preferences, show its dialog
if (dialogFragment != null) {
final FragmentManager fm = this.getParentFragmentManager();
// check if dialog is already showing
if (fm.findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) {
return;
}
dialogFragment.setTargetFragment(this, 0);
dialogFragment.show(fm, DIALOG_FRAGMENT_TAG);
}
// Could not be handled here. Try with the super method.
else {
super.onDisplayPreferenceDialog(preference);
}
}
private static Pair generateAppToRunListContent(@NonNull Context context) {
List appEntryList = TBApplication.appsHandler(context).getApplications();
Collections.sort(appEntryList, AppEntry.NAME_COMPARATOR);
final int appCount = appEntryList.size();
CharSequence[] entries = new CharSequence[appCount];
CharSequence[] entryValues = new CharSequence[appCount];
for (int idx = 0; idx < appCount; idx++) {
AppEntry appEntry = appEntryList.get(idx);
entries[idx] = appEntry.getName();
entryValues[idx] = appEntry.getUserComponentName();
}
return new Pair<>(entries, entryValues);
}
private static Pair generateShortcutToRunListContent(@NonNull Context context) {
ShortcutsProvider shortcutsProvider = TBApplication.dataHandler(context).getShortcutsProvider();
List extends EntryItem> shortcutList = shortcutsProvider == null ? null : shortcutsProvider.getPojos();
if (shortcutList == null)
return new Pair<>(new CharSequence[0], new CharSequence[0]);
// copy list in order to sort it
shortcutList = new ArrayList<>(shortcutList);
Collections.sort(shortcutList, EntryItem.NAME_COMPARATOR);
final int entryCount = shortcutList.size();
CharSequence[] entries = new CharSequence[entryCount];
CharSequence[] entryValues = new CharSequence[entryCount];
for (int idx = 0; idx < entryCount; idx++) {
EntryItem shortcutEntry = shortcutList.get(idx);
entries[idx] = shortcutEntry.getName();
entryValues[idx] = shortcutEntry.id;
}
return new Pair<>(entries, entryValues);
}
private static Pair generateEntryToShowListContent(@NonNull Context context) {
final List tagList;
final TBApplication app = TBApplication.getApplication(context);
final TagsProvider tagsProvider = app.getDataHandler().getTagsProvider();
if (tagsProvider != null) {
ArrayList tagNames = new ArrayList<>(app.tagsHandler().getValidTags());
Collections.sort(tagNames);
tagList = new ArrayList<>(tagNames.size());
for (String tagName : tagNames) {
TagEntry tagEntry = tagsProvider.getTagEntry(tagName);
tagList.add(tagEntry);
}
} else {
tagList = Collections.emptyList();
}
final CharSequence[] entries;
final CharSequence[] entryValues;
if (tagList.isEmpty()) {
entries = new CharSequence[]{context.getString(R.string.no_tags)};
entryValues = new CharSequence[]{""};
} else {
return ContentLoadHelper.generateStaticEntryList(context, tagList);
}
return new Pair<>(entries, entryValues);
}
private static Pair generateMimeTypeListContent(@NonNull Context context) {
Set supportedMimeTypes = MimeTypeUtils.getSupportedMimeTypes(context);
Map labels = TBApplication.mimeTypeCache(context).getUniqueLabels(context, supportedMimeTypes);
String[] mimeTypes = labels.keySet().toArray(new String[0]);
Arrays.sort(mimeTypes);
CharSequence[] mimeLabels = new CharSequence[mimeTypes.length];
for (int index = 0; index < mimeTypes.length; index += 1) {
mimeLabels[index] = labels.get(mimeTypes[index]);
}
return new Pair<>(mimeTypes, mimeLabels);
}
private void updateListPrefDependency(@NonNull String dependOnKey, @Nullable String dependOnValue, @NonNull String enableValue, @NonNull String listKey, @Nullable Pair listContent) {
Preference prefEntryToRun = findPreference(listKey);
if (prefEntryToRun instanceof ListPreference) {
synchronized (SettingsFragment.class) {
if (listContent != null) {
CharSequence[] entries = listContent.first;
CharSequence[] entryValues = listContent.second;
((ListPreference) prefEntryToRun).setEntries(entries);
((ListPreference) prefEntryToRun).setEntryValues(entryValues);
prefEntryToRun.setVisible(enableValue.equals(dependOnValue));
return;
}
}
}
if (prefEntryToRun == null) {
// the ListPreference for selecting an app is missing. Remove the option to run an app.
Preference pref = findPreference(dependOnKey);
if (pref instanceof ListPreference) {
removeEntryValueFromListPreference(enableValue, (ListPreference) pref);
}
} else {
Log.w(TAG, "ListPreference `" + listKey + "` can't be updated");
prefEntryToRun.setVisible(false);
}
}
private void updateAppToRunList(@NonNull SharedPreferences sharedPreferences, String key) {
updateListPrefDependency(key, sharedPreferences.getString(key, null), "runApp", key + "-app-to-run", AppToRunListContent);
}
private void updateShortcutToRunList(@NonNull SharedPreferences sharedPreferences, String key) {
updateListPrefDependency(key, sharedPreferences.getString(key, null), "runShortcut", key + "-shortcut-to-run", ShortcutToRunListContent);
}
private void updateEntryToShowList(@NonNull SharedPreferences sharedPreferences, String key) {
updateListPrefDependency(key, sharedPreferences.getString(key, null), "showEntry", key + "-entry-to-show", EntryToShowListContent);
}
private static void removeEntryValueFromListPreference(@NonNull String entryValueToRemove, ListPreference listPref) {
CharSequence[] entryValues = listPref.getEntryValues();
int indexToRemove = -1;
for (int idx = 0, entryValuesLength = entryValues.length; idx < entryValuesLength; idx++) {
CharSequence entryValue = entryValues[idx];
if (entryValueToRemove.contentEquals(entryValue)) {
indexToRemove = idx;
break;
}
}
if (indexToRemove == -1)
return;
CharSequence[] entries = listPref.getEntries();
final int size = entries.length;
final int newSize = size - 1;
CharSequence[] newEntries = new CharSequence[newSize];
CharSequence[] newEntryValues = new CharSequence[newSize];
if (indexToRemove > 0) {
System.arraycopy(entries, 0, newEntries, 0, indexToRemove);
System.arraycopy(entryValues, 0, newEntryValues, 0, indexToRemove);
}
if (indexToRemove < newSize) {
System.arraycopy(entries, indexToRemove + 1, newEntries, indexToRemove, newSize - indexToRemove);
System.arraycopy(entryValues, indexToRemove + 1, newEntryValues, indexToRemove, newSize - indexToRemove);
}
listPref.setEntries(newEntries);
listPref.setEntryValues(newEntryValues);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
SettingsActivity activity = (SettingsActivity) getActivity();
if (activity == null || key == null)
return;
if (PREF_LISTS_WITH_DEPENDENCY.contains(key)) {
updateAppToRunList(sharedPreferences, key);
updateShortcutToRunList(sharedPreferences, key);
updateEntryToShowList(sharedPreferences, key);
}
// rebind and relayout all visible views because I can't find how to rebind only the current view
getListView().getAdapter().notifyDataSetChanged();
SettingsActivity.onSharedPreferenceChanged(activity, sharedPreferences, key);
synchronized (SettingsFragment.class) {
if (TagsMenuContent != null) {
if ("tags-menu-list".equals(key) || "tags-menu-order".equals(key)) {
TagsMenuContent.reloadOrderedValues(sharedPreferences, this, "tags-menu-order");
} else if ("result-popup-order".equals(key)) {
ResultPopupContent.reloadOrderedValues(sharedPreferences, this, "result-popup-order");
}
}
}
}
}
@SuppressWarnings("deprecation")
private static void setActionBarTextColor(Activity activity, int color) {
ActionBar actionBar = activity instanceof AppCompatActivity
? ((AppCompatActivity) activity).getSupportActionBar()
: null;
CharSequence title = actionBar != null ? actionBar.getTitle() : null;
if (title == null)
return;
activity.setTitleColor(color);
Drawable arrow = AppCompatResources.getDrawable(activity, R.drawable.ic_arrow_back);
if (arrow != null) {
arrow = DrawableCompat.wrap(arrow);
DrawableCompat.setTint(arrow, color);
actionBar.setHomeAsUpIndicator(arrow);
}
SpannableString text = new SpannableString(title);
ForegroundColorSpan[] spansToRemove = text.getSpans(0, text.length(), ForegroundColorSpan.class);
for (ForegroundColorSpan span : spansToRemove) {
text.removeSpan(span);
}
text.setSpan(new ForegroundColorSpan(color), 0, text.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
actionBar.setTitle(text);
}
private static void applyNotificationBarColor(@NonNull SharedPreferences sharedPreferences, @Nullable Context context) {
int color = UIColors.getColor(sharedPreferences, "notification-bar-argb");
// keep the bars opaque to avoid white text on white background by mistake
int alpha = 0xFF;//UIColors.getAlpha(sharedPreferences, "notification-bar-alpha");
Activity activity = Utilities.getActivity(context);
if (activity instanceof SettingsActivity)
UIColors.setStatusBarColor((SettingsActivity) activity, UIColors.setAlpha(color, alpha));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
View view = activity != null ? activity.findViewById(android.R.id.content) : null;
if (view == null && activity != null)
view = activity.getWindow() != null ? activity.getWindow().getDecorView() : null;
if (view != null) {
if (sharedPreferences.getBoolean("black-notification-icons", false)) {
SystemUiVisibility.setLightStatusBar(view);
} else {
SystemUiVisibility.clearLightStatusBar(view);
}
}
}
setActionBarTextColor(activity, UIColors.getTextContrastColor(color));
}
private static void applyNavigationBarColor(@NonNull SharedPreferences sharedPreferences, @Nullable Context context) {
int color = UIColors.getColor(sharedPreferences, "navigation-bar-argb");
Activity activity = Utilities.getActivity(context);
if (activity instanceof SettingsActivity)
UIColors.setNavigationBarColor((SettingsActivity) activity, color, color);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
View view = activity != null ? activity.findViewById(android.R.id.content) : null;
if (view == null && activity != null)
view = activity.getWindow() != null ? activity.getWindow().getDecorView() : null;
if (view != null) {
if (UIColors.isColorLight(color)) {
SystemUiVisibility.setLightNavigationBar(view);
} else {
SystemUiVisibility.clearLightNavigationBar(view);
}
}
}
}
public static void onSharedPreferenceChanged(Context context, SharedPreferences sharedPreferences, String key) {
TBApplication app = TBApplication.getApplication(context);
if (PREF_THAT_REQUIRE_LAYOUT_UPDATE.contains(key))
app.requireLayoutUpdate();
TBLauncherActivity activity = app.launcherActivity();
if (activity != null)
activity.liveWallpaper.onPrefChanged(sharedPreferences, key);
switch (key) {
case "notification-bar-argb":
case "black-notification-icons":
applyNotificationBarColor(sharedPreferences, context);
break;
case "navigation-bar-argb":
applyNavigationBarColor(sharedPreferences, context);
break;
case "icon-scale-red":
case "icon-scale-green":
case "icon-scale-blue":
case "icon-scale-alpha":
case "icon-hue":
case "icon-contrast":
case "icon-brightness":
case "icon-saturation":
case "icon-background-argb":
case "matrix-contacts":
case "icons-visible":
TBApplication.drawableCache(context).clearCache();
if (activity != null)
activity.refreshSearchRecords();
// fallthrough
case "quick-list-argb":
case "quick-list-ripple-color":
// static entities will change color based on luminance
// fallthrough
case "quick-list-toggle-color":
// toggle animation is also caching the color
if (activity != null)
activity.queueDockReload();
// fallthrough
case "result-list-argb":
case "result-ripple-color":
case "result-highlight-color":
case "result-text-color":
case "result-text2-color":
case "result-shadow-color":
case "contact-action-color":
if (activity != null)
activity.refreshSearchRecords();
// fallthrough
case "search-bar-text-color":
case "search-bar-shadow-color":
case "popup-background-argb":
case "popup-border-argb":
case "popup-ripple-color":
case "popup-text-color":
case "popup-title-color":
case "popup-shadow-color":
UIColors.resetCache();
break;
case "quick-list-icon-size":
if (activity != null)
activity.queueDockReload();
// fallthrough
case "result-text-size":
case "result-text2-size":
case "result-icon-size":
case "result-shadow-radius":
case "result-shadow-dx":
case "result-shadow-dy":
case "result-list-row-height":
if (activity != null)
activity.refreshSearchRecords();
// fallthrough
case "tags-menu-icon-size":
case "search-bar-shadow-dx":
case "search-bar-shadow-dy":
case "search-bar-shadow-radius":
case "popup-corner-radius":
case "popup-shadow-dx":
case "popup-shadow-dy":
case "popup-shadow-radius":
UISizes.resetCache();
break;
case "result-history-size":
case "result-history-adaptive":
case "fuzzy-search-tags":
case "result-search-cap":
case "tags-menu-icons":
case "loading-icon":
case "tags-menu-untagged":
case "tags-menu-untagged-index":
case "result-popup-order":
PrefCache.resetCache();
break;
case "adaptive-shape":
case "force-adaptive":
case "force-shape":
case "icons-pack":
case "contact-pack-mask":
case "contacts-shape":
case "shortcut-pack-mask":
case "shortcut-shape":
case "shortcut-pack-badge-mask":
TBApplication.iconsHandler(context).onPrefChanged(sharedPreferences);
TBApplication.drawableCache(context).clearCache();
if (activity != null)
activity.queueDockReload();
break;
case "tags-enabled": {
boolean useTags = sharedPreferences.getBoolean("tags-enabled", true);
Activity settingsActivity = Utilities.getActivity(context);
Fragment fragment = null;
if (settingsActivity instanceof SettingsActivity)
fragment = ((SettingsActivity) settingsActivity).getSupportFragmentManager().findFragmentByTag(SettingsFragment.FRAGMENT_TAG);
SwitchPreference preference = null;
if (fragment instanceof SettingsFragment)
preference = ((SettingsFragment) fragment).findPreference("fuzzy-search-tags");
if (preference != null)
preference.setChecked(useTags);
else
sharedPreferences.edit().putBoolean("fuzzy-search-tags", useTags).apply();
break;
}
case "quick-list-enabled":
case "quick-list-text-visible":
case "quick-list-icons-visible":
case "quick-list-show-badge":
case "quick-list-columns":
case "quick-list-rows":
case "quick-list-rtl":
if (activity != null)
activity.queueDockReload();
break;
case "cache-drawable":
case "cache-half-apps":
TBApplication.drawableCache(context).onPrefChanged(context, sharedPreferences);
break;
case "enable-search":
case "enable-url":
case "enable-calculator":
case "enable-dial":
case "enable-contacts":
case "selected-contact-mime-types":
case "shortcut-dynamic-in-results":
TBApplication.dataHandler(context).reloadProviders();
break;
case "root-mode":
if (sharedPreferences.getBoolean("root-mode", false) &&
!TBApplication.rootHandler(context).isRootAvailable()) {
//show error dialog
new AlertDialog.Builder(context).setMessage(R.string.root_mode_error)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
sharedPreferences.edit().putBoolean("root-mode", false).apply();
}).show();
}
TBApplication.rootHandler(context).resetRootHandler(sharedPreferences);
break;
case "tags-menu-list":
PrefOrderedListHelper.syncOrderedList(sharedPreferences, "tags-menu-list", "tags-menu-order");
break;
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/TBApplication.java
================================================
package rocks.tbog.tblauncher;
import android.app.Activity;
import android.app.Application;
import android.content.ComponentCallbacks2;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import androidx.preference.PreferenceManager;
import org.acra.ACRA;
import org.acra.config.CoreConfigurationBuilder;
import org.acra.config.DialogConfigurationBuilder;
import org.acra.config.MailSenderConfigurationBuilder;
import org.acra.data.StringFormat;
import java.lang.ref.WeakReference;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.LinkedList;
import rocks.tbog.tblauncher.handler.AppsHandler;
import rocks.tbog.tblauncher.handler.DataHandler;
import rocks.tbog.tblauncher.handler.IconsHandler;
import rocks.tbog.tblauncher.handler.TagsHandler;
import rocks.tbog.tblauncher.icons.IconPackCache;
import rocks.tbog.tblauncher.quicklist.QuickList;
import rocks.tbog.tblauncher.searcher.Searcher;
import rocks.tbog.tblauncher.ui.ListPopup;
import rocks.tbog.tblauncher.utils.PrefCache;
import rocks.tbog.tblauncher.utils.RootHandler;
import rocks.tbog.tblauncher.utils.Utilities;
import rocks.tbog.tblauncher.widgets.WidgetManager;
public class TBApplication extends Application {
private static final String TAG = "APP";
/**
* The state of certain launcher features
*/
@NonNull
private static final LauncherState mState = new LauncherState();
private DataHandler dataHandler = null;
private IconsHandler iconsPackHandler = null;
private TagsHandler tagsHandler = null;
private AppsHandler appsHandler = null;
private SharedPreferences mSharedPreferences = null;
private ListPopup mPopup = null;
/**
* List of running launcher activities
*/
private final LinkedList> mActivities = new LinkedList<>();
/**
* Task launched on text change
*/
private Searcher mSearchTask;
/**
* We store a number of drawables in memory for fast redraw
*/
private final DrawableCache mDrawableCache = new DrawableCache();
/**
* We store a number of icon packs so we don't have to parse the XML
*/
private final IconPackCache mIconPackCache = new IconPackCache();
/**
* We store a number of icon packs so we don't have to parse the XML
*/
private final MimeTypeCache mMimeTypeCache = new MimeTypeCache();
/**
* Root handler - su
*/
private RootHandler mRootHandler = null;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//MultiDex.install(this);
ACRA.init(this, new CoreConfigurationBuilder()
.withBuildConfigClass(BuildConfig.class)
.withReportFormat(StringFormat.JSON)
.withPluginConfigurations(new MailSenderConfigurationBuilder()
.withMailTo("tblauncher.acra@tbog.rocks")
.withReportAsFile(false)
.build()
, new DialogConfigurationBuilder()
.withTitle(getString(R.string.crash_title))
.withText(getString(R.string.crash_text))
.withPositiveButtonText(getString(R.string.crash_send_email))
.withResTheme(R.style.TitleDialogTheme)
.build()));
}
@NonNull
public static TBApplication getApplication(@NonNull Context context) {
Context appContext = context.getApplicationContext();
if (appContext instanceof TBApplication)
return (TBApplication) appContext;
throw new IllegalStateException("appContext " + appContext + " not of type " + TBApplication.class.getSimpleName());
}
@NonNull
private TBLauncherActivity validateActivity(@NonNull Context context) {
Activity activity = Utilities.getActivity(context);
if (activity == null)
throw new IllegalStateException("context " + context + " null activity");
TBLauncherActivity foundActivity = null;
for (WeakReference ref : mActivities) {
TBLauncherActivity launcherActivity = ref.get();
if (launcherActivity == activity)
foundActivity = launcherActivity;
}
if (foundActivity == null)
throw new IllegalStateException("activity " + activity + " not registered");
return foundActivity;
}
@NonNull
private TBLauncherActivity getActivity() {
WeakReference ref = mActivities.peekFirst();
if (ref == null)
throw new IllegalStateException("no activity registered");
TBLauncherActivity launcherActivity = ref.get();
while (launcherActivity == null) {
if (!mActivities.remove(ref))
throw new ConcurrentModificationException();
ref = mActivities.peekFirst();
if (ref == null)
throw new IllegalStateException("all registered activities released");
launcherActivity = ref.get();
}
if (launcherActivity.getLifecycle().getCurrentState().compareTo(Lifecycle.State.DESTROYED) == 0)
throw new IllegalStateException("activity destroyed");
return launcherActivity;
}
/**
* There should be only one activity, but for short periods of time there can be:
* - none when launcher got shut down for memory reasons
* - two when the activity gets recreated (user pressed the "home" button for example)
*
* @return most recently registered launcher activity or null
*/
@Nullable
public TBLauncherActivity launcherActivity() {
WeakReference ref = mActivities.peekFirst();
TBLauncherActivity launcherActivity = ref == null ? null : ref.get();
if (launcherActivity != null && launcherActivity.getLifecycle().getCurrentState().compareTo(Lifecycle.State.DESTROYED) == 0)
return null;
Log.d(TAG, "launcherActivity=" + launcherActivity);
return launcherActivity;
}
/**
* Same as the getting application from context then calling launcherActivity()
*
* @param context to get application from
* @return most recently registered launcher activity or null
*/
@Nullable
public static TBLauncherActivity launcherActivity(@NonNull Context context) {
return getApplication(context).launcherActivity();
}
public static boolean activityInvalid(@Nullable View view) {
if (view != null && view.isAttachedToWindow())
return activityInvalid(view.getContext());
return false;
}
public static boolean activityInvalid(@Nullable Context ctx) {
return !activityValid(ctx);
}
public static boolean activityValid(@Nullable Context context) {
Context ctx = context;
while (ctx instanceof ContextWrapper) {
if (ctx instanceof Activity) {
Activity act = (Activity) ctx;
if (act.isFinishing() || act.isDestroyed()) {
// activity is no more
return false;
}
TBApplication app = getApplication(act);
for (WeakReference ref : app.mActivities) {
TBLauncherActivity launcherActivity = ref.get();
if (act.equals(launcherActivity)) {
Lifecycle.State state = launcherActivity.getLifecycle().getCurrentState();
return state.isAtLeast(Lifecycle.State.INITIALIZED);
}
}
// activity not registered
return false;
}
ctx = ((ContextWrapper) ctx).getBaseContext();
}
// context is null
return false;
}
public void onCreateActivity(TBLauncherActivity activity) {
// clean list
for (Iterator> iterator = mActivities.iterator(); iterator.hasNext(); ) {
WeakReference ref = iterator.next();
TBLauncherActivity launcherActivity = ref.get();
if (launcherActivity == null)
iterator.remove();
}
// add to list
mActivities.push(new WeakReference<>(activity));
Log.d(TAG, "activities.size=" + mActivities.size());
}
@NonNull
public SharedPreferences preferences() {
return mSharedPreferences;
}
public static Behaviour behaviour(@NonNull Context context) {
TBApplication app = getApplication(context);
return app.validateActivity(context).behaviour;
}
@NonNull
public Behaviour behaviour() {
return getActivity().behaviour;
}
@NonNull
public static LiveWallpaper liveWallpaper(Context context) {
TBApplication app = getApplication(context);
return app.validateActivity(context).liveWallpaper;
}
public static QuickList quickList(Context context) {
TBApplication app = getApplication(context);
return app.validateActivity(context).quickList;
}
public static CustomizeUI ui(Context context) {
TBApplication app = getApplication(context);
return app.validateActivity(context).customizeUI;
}
@NonNull
public static WidgetManager widgetManager(Context context) {
TBApplication app = getApplication(context);
return app.validateActivity(context).widgetManager;
}
@NonNull
public static DrawableCache drawableCache(Context context) {
return getApplication(context).mDrawableCache;
}
@NonNull
public static IconPackCache iconPackCache(Context context) {
return getApplication(context).mIconPackCache;
}
@NonNull
public static MimeTypeCache mimeTypeCache(Context context) {
return getApplication(context).mMimeTypeCache;
}
@NonNull
public static TagsHandler tagsHandler(Context context) {
return getApplication(context).tagsHandler();
}
@NonNull
public static AppsHandler appsHandler(Context context) {
return getApplication(context).appsHandler();
}
@NonNull
public static DataHandler dataHandler(Context context) {
return getApplication(context).getDataHandler();
}
@NonNull
public static RootHandler rootHandler(Context context) {
return getApplication(context).rootHandler();
}
@NonNull
public static LauncherState state() {
return mState;
}
public static void onDestroyActivity(TBLauncherActivity activity) {
TBApplication app = getApplication(activity);
Activity popupActivity = null;
if (app.mPopup != null)
popupActivity = Utilities.getActivity(app.mPopup.getContentView());
if (popupActivity == null && app.dismissPopup())
Log.i(TAG, "Popup dismissed in onDestroyActivity");
for (Iterator> iterator = app.mActivities.iterator(); iterator.hasNext(); ) {
WeakReference ref = iterator.next();
TBLauncherActivity launcherActivity = ref.get();
if (launcherActivity == null || launcherActivity == activity) {
if (activity == popupActivity && app.dismissPopup())
Log.i(TAG, "Popup dismissed in onDestroyActivity " + activity);
iterator.remove();
}
}
}
public static void runTask(Context context, Searcher task) {
resetTask(context);
getApplication(context).mSearchTask = task;
task.execute();
}
public static void resetTask(Context context) {
TBApplication app = getApplication(context);
if (app.mSearchTask != null) {
app.mSearchTask.cancel(true);
app.mSearchTask = null;
}
}
public static boolean hasSearchTask(Context context) {
TBApplication app = getApplication(context);
return app.mSearchTask != null;
}
public static IconsHandler iconsHandler(Context ctx) {
return getApplication(ctx).iconsHandler();
}
public static boolean isDefaultLauncher(Context context) {
String homePackage;
try {
Intent i = new Intent(Intent.ACTION_MAIN);
i.addCategory(Intent.CATEGORY_HOME);
PackageManager pm = context.getPackageManager();
final ResolveInfo mInfo = pm.resolveActivity(i, PackageManager.MATCH_DEFAULT_ONLY);
homePackage = mInfo == null ? "null" : mInfo.activityInfo.packageName;
} catch (Exception e) {
homePackage = "unknown";
}
return homePackage.equals(context.getPackageName());
}
public static void resetDefaultLauncherAndOpenChooser(Context context) {
PackageManager packageManager = context.getPackageManager();
ComponentName componentName = new ComponentName(context, DummyLauncherActivity.class);
packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
Intent selector = new Intent(Intent.ACTION_MAIN);
selector.addCategory(Intent.CATEGORY_HOME);
selector.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(selector);
packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, PackageManager.DONT_KILL_APP);
}
@Override
public void onCreate() {
super.onCreate();
PreferenceManager.setDefaultValues(this, R.xml.preferences, true);
PreferenceManager.setDefaultValues(this, R.xml.preference_features, true);
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
if (PrefCache.isMigrateRequired(mSharedPreferences) && PrefCache.migratePreferences(this, mSharedPreferences)) {
Log.i(TAG, "Preferences migration done.");
}
// SharedPreferences.Editor editor = mSharedPreferences.edit();
// for (Map.Entry entry : mSharedPreferences.getAll().entrySet() )
// {
// if (entry.getKey().startsWith("gesture-")) {
// Log.d("Pref", entry.getKey() + "=" + entry.getValue());
// editor.putString(entry.getKey(), "none");
// }
// }
// editor.commit();
mDrawableCache.onPrefChanged(this, mSharedPreferences);
}
@Override
public void onTerminate() {
TBLauncherActivity launcherActivity = launcherActivity();
if (launcherActivity != null)
launcherActivity.widgetManager.stop();
super.onTerminate();
}
@NonNull
public TagsHandler tagsHandler() {
if (tagsHandler == null)
tagsHandler = new TagsHandler(this);
return tagsHandler;
}
@NonNull
public AppsHandler appsHandler() {
if (appsHandler == null)
appsHandler = new AppsHandler(this);
return appsHandler;
}
@NonNull
public DataHandler getDataHandler() {
synchronized (this) {
if (dataHandler == null) {
dataHandler = new DataHandler(this);
}
}
return dataHandler;
}
@NonNull
public DrawableCache drawableCache() {
return mDrawableCache;
}
public void initDataHandler() {
synchronized (this) {
if (dataHandler == null) {
dataHandler = new DataHandler(this);
}
}
if (dataHandler.fullLoadOverSent()) {
// Already loaded! We still need to fire the FULL_LOAD event
DataHandler.sendBroadcast(this, TBLauncherActivity.FULL_LOAD_OVER, TAG);
}
}
@NonNull
public IconsHandler iconsHandler() {
if (iconsPackHandler == null) {
iconsPackHandler = new IconsHandler(this);
}
return iconsPackHandler;
}
public void resetIconsHandler() {
iconsPackHandler = new IconsHandler(this);
}
@NonNull
public RootHandler rootHandler() {
if (mRootHandler == null)
mRootHandler = new RootHandler(mSharedPreferences);
return mRootHandler;
}
public void requireLayoutUpdate() {
for (WeakReference ref : mActivities) {
TBLauncherActivity launcherActivity = ref.get();
if (launcherActivity != null)
launcherActivity.requireLayoutUpdate();
}
}
public void registerPopup(ListPopup popup) {
if (mPopup == popup)
return;
dismissPopup();
mPopup = popup;
popup.setOnDismissListener(() -> mPopup = null);
}
public boolean dismissPopup() {
if (mPopup != null) {
mPopup.dismiss();
return true;
}
return false;
}
@Nullable
public ListPopup getPopup() {
return mPopup;
}
/**
* Release memory when the UI becomes hidden or when system resources become low.
*
* @param level the memory-related event that was raised.
*/
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// the process had been showing a user interface, and is no longer doing so
mDrawableCache.clearCache();
}
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
// this is called every time the screen is off
SQLiteDatabase.releaseMemory();
mIconPackCache.clearCache(this);
if (mSharedPreferences.getBoolean("screen-off-cache-clear", false))
mDrawableCache.clearCache();
mMimeTypeCache.clearCache();
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/TBLauncherActivity.java
================================================
package rocks.tbog.tblauncher;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import rocks.tbog.tblauncher.quicklist.QuickList;
import rocks.tbog.tblauncher.ui.ListPopup;
import rocks.tbog.tblauncher.utils.DebugInfo;
import rocks.tbog.tblauncher.utils.DeviceUtils;
import rocks.tbog.tblauncher.widgets.WidgetManager;
public class TBLauncherActivity extends AppCompatActivity implements ActivityCompat.OnRequestPermissionsResultCallback {
private static final String TAG = "TBL";
public static final String START_LOAD = "rocks.tbog.provider.START_LOAD";
public static final String LOAD_OVER = "rocks.tbog.provider.LOAD_OVER";
public static final String FULL_LOAD_OVER = "rocks.tbog.provider.FULL_LOAD_OVER";
public static final String INTENT_DATA = "rocks.tbog.provider.INTENT_DATA";
/**
* Receive events from providers
*/
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "BroadcastReceiver action=`" + intent.getAction() + "` extra=`" + intent.getStringExtra(INTENT_DATA) + "`");
if (START_LOAD.equalsIgnoreCase(intent.getAction())) {
behaviour.displayLoader(true);
} else if (LOAD_OVER.equalsIgnoreCase(intent.getAction())) {
behaviour.updateSearchRecords();
} else if (FULL_LOAD_OVER.equalsIgnoreCase(intent.getAction())) {
Log.v(TAG, "All providers are done loading.");
TBApplication app = TBApplication.getApplication(TBLauncherActivity.this);
app.getDataHandler().executeAfterLoadOverTasks();
behaviour.displayLoader(false);
SharedPreferences prefs = app.preferences();
// we need to set drawable cache preferences after we load all the apps
app.drawableCache().onPrefChanged(TBLauncherActivity.this, prefs);
// make sure we load the icon pack as early as possible
app.iconsHandler().onPrefChanged(prefs);
// Run GC once to free all the garbage accumulated during provider initialization
System.gc();
}
updateTextView(debugTextView);
}
};
private Permission permissionManager;
private TextView debugTextView;
/**
* Everything that has to do with the UI behaviour
*/
public final Behaviour behaviour = new Behaviour();
/**
* Manage live wallpaper interaction
*/
public final LiveWallpaper liveWallpaper = new LiveWallpaper();
/**
* The dock / quick access bar
*/
public final QuickList quickList = new QuickList();
/**
* Everything that has to do with the UI customization (drawables and colors)
*/
public final CustomizeUI customizeUI = new CustomizeUI();
/**
* Manage widgets
*/
public final WidgetManager widgetManager = new WidgetManager();
private boolean bLayoutUpdateRequired = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
widgetManager.start(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
final TBApplication app = TBApplication.getApplication(this);
app.onCreateActivity(this);
/*
* Initialize preferences
*/
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
var prefs = PreferenceManager.getDefaultSharedPreferences(this);
Behaviour.setActivityOrientation(this, prefs);
/*
* Permission Manager
*/
permissionManager = new Permission(this);
/*
* Initialize data handler and start loading providers
*/
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(START_LOAD);
intentFilter.addAction(LOAD_OVER);
intentFilter.addAction(FULL_LOAD_OVER);
ActivityCompat.registerReceiver(this, mReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
// init DataHandler after we register the receiver
app.initDataHandler();
setContentView(R.layout.activity_fullscreen);
debugTextView = findViewById(R.id.debugText);
if (BuildConfig.DEBUG) {
DeviceUtils.showDeviceInfo("TBLauncher", this);
}
Log.d(TAG, "onCreateActivity(" + this + ")");
// call after all views are set
behaviour.onCreateActivity(this);
customizeUI.onCreateActivity(this);
quickList.onCreateActivity(this);
liveWallpaper.onCreateActivity(this);
widgetManager.onCreateActivity(this);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
Log.d(TAG, "dispatchKeyEvent " + event);
return super.dispatchKeyEvent(event);
}
@Override
protected void onStart() {
Log.d(TAG, "onStart(" + this + ")");
super.onStart();
if (DebugInfo.providerStatus(this)) {
debugTextView.setVisibility(View.VISIBLE);
}
behaviour.onStart();
customizeUI.onStart();
quickList.onStart();
}
@Override
protected void onStop() {
Log.d(TAG, "onStop(" + this + ")");
super.onStop();
}
@Override
protected void onRestart() {
Log.d(TAG, "onRestart(" + this + ")");
super.onRestart();
}
@Override
protected void onDestroy() {
Log.d(TAG, "onDestroy(" + this + ")");
if (behaviour.closeFragmentDialog()) {
Log.i(TAG, "closed dialog from onDestroy " + this);
}
TBApplication.onDestroyActivity(this);
unregisterReceiver(mReceiver);
widgetManager.stop();
super.onDestroy();
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
//TBApplication.behaviour(this).onConfigurationChanged(this, newConfig);
Log.d(TAG, "onConfigurationChanged" +
" orientation=" + newConfig.orientation +
" keyboard=" + newConfig.keyboard +
" keyboardHidden=" + newConfig.keyboardHidden);
super.onConfigurationChanged(newConfig);
}
public boolean isLayoutUpdateRequired() {
return bLayoutUpdateRequired;
}
public void requireLayoutUpdate(boolean require) {
bLayoutUpdateRequired = require;
}
public void requireLayoutUpdate() {
bLayoutUpdateRequired = true;
}
@Override
protected void onResume() {
Log.d(TAG, "onResume(" + this + ")");
super.onResume();
if (isLayoutUpdateRequired()) {
requireLayoutUpdate(false);
Log.i(TAG, "Restarting app after setting changes");
// Restart current activity to refresh view, since some preferences may require using a new UI
//getWindow().getDecorView().post(TBLauncherActivity.this::recreate);
Log.d(TAG, "finish(" + this + ")");
finish();
startActivity(new Intent(this, getClass()));
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
return;
}
behaviour.onResume();
}
@Override
protected void onNewIntent(Intent intent) {
Log.d(TAG, "onNewIntent(" + this + ")");
setIntent(intent);
super.onNewIntent(intent);
// This is called when the user press Home again while already browsing MainActivity
// onResume() will be called right after, hiding the kissbar if any.
// http://developer.android.com/reference/android/app/Activity.html#onNewIntent(android.content.Intent)
// Animation can't happen in this method, since the activity is not resumed yet, so they'll happen in the onResume()
// https://github.com/Neamar/KISS/issues/569
behaviour.onNewIntent();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
Log.i(TAG, "onSaveInstanceState " + Integer.toHexString(outState.hashCode()) + " " + this);
super.onSaveInstanceState(outState);
outState.clear();
}
@Override
public boolean onKeyDown(int keycode, KeyEvent e) {
// For devices with a physical menu button, we still want to display *our* contextual menu
if (keycode == KeyEvent.KEYCODE_MENU) {
behaviour.showContextMenu();
return true;
}
return super.onKeyDown(keycode, e);
}
@Override
public void onBackPressed() {
if (TBApplication.getApplication(this).dismissPopup())
return;
if (behaviour.onBackPressed())
return;
super.onBackPressed();
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
behaviour.onWindowFocusChanged(hasFocus);
}
// /**
// * Schedules a call to hide() in delay milliseconds, canceling any
// * previously scheduled calls.
// */
// private void delayedHide(int delayMillis) {
// mHideHandler.removeCallbacks(mHideRunnable);
// mHideHandler.postDelayed(mHideRunnable, delayMillis);
// }
public void queueDockReload() {
quickList.reload();
}
public void refreshSearchRecords() {
behaviour.refreshSearchRecords();
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
boolean shouldDismissPopup = false;
ListPopup listPopup = TBApplication.getApplication(this).getPopup();
if (listPopup != null) {
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
// this check is not needed
// we'll not receive the event if it happened inside the popup
int x = (int) (event.getRawX() + .5f);
int y = (int) (event.getRawY() + .5f);
if (!listPopup.isInsideViewBounds(x, y))
shouldDismissPopup = true;
}
}
if (shouldDismissPopup && TBApplication.getApplication(this).dismissPopup())
return true;
return super.dispatchTouchEvent(event);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
permissionManager.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
Context c = this;
while (null != c) {
Log.d(TAG, "Ctx: " + c.toString() + " | Res: " + c.getResources().toString());
if (c instanceof ContextWrapper)
c = ((ContextWrapper) c).getBaseContext();
else
c = null;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (widgetManager.onActivityResult(this, requestCode, resultCode, data))
return;
super.onActivityResult(requestCode, resultCode, data);
}
private void updateTextView(TextView debugTextView) {
if (debugTextView == null)
return;
StringBuilder text = new StringBuilder();
TBApplication app = TBApplication.getApplication(this);
app.getDataHandler().appendDebugText(text);
debugTextView.setText(text);
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/TagsManager.java
================================================
package rocks.tbog.tblauncher;
import android.app.Activity;
import android.content.Context;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Objects;
import rocks.tbog.tblauncher.WorkAsync.RunnableTask;
import rocks.tbog.tblauncher.WorkAsync.TaskRunner;
import rocks.tbog.tblauncher.customicon.IconSelectDialog;
import rocks.tbog.tblauncher.dataprovider.IProvider;
import rocks.tbog.tblauncher.dataprovider.TagsProvider;
import rocks.tbog.tblauncher.drawable.CodePointDrawable;
import rocks.tbog.tblauncher.drawable.DrawableUtils;
import rocks.tbog.tblauncher.entry.ActionEntry;
import rocks.tbog.tblauncher.entry.EntryItem;
import rocks.tbog.tblauncher.entry.StaticEntry;
import rocks.tbog.tblauncher.entry.TagEntry;
import rocks.tbog.tblauncher.handler.AppsHandler;
import rocks.tbog.tblauncher.handler.DataHandler;
import rocks.tbog.tblauncher.handler.IconsHandler;
import rocks.tbog.tblauncher.handler.TagsHandler;
import rocks.tbog.tblauncher.result.ResultViewHelper;
import rocks.tbog.tblauncher.utils.DialogHelper;
import rocks.tbog.tblauncher.utils.Utilities;
import rocks.tbog.tblauncher.utils.ViewHolderAdapter;
import rocks.tbog.tblauncher.utils.ViewHolderListAdapter;
public class TagsManager {
private static final String TAG = "TagMgr";
private final ArrayList mTagList = new ArrayList<>();
private ListView mListView;
private TagsAdapter mAdapter;
public interface OnItemClickListener {
void onItemClickListener(@NonNull View view, @NonNull TagInfo tagInfo);
}
public boolean hasChangesMade() {
for (TagInfo tagInfo : mTagList) {
if (tagInfo.action != TagInfo.Action.NONE)
return true;
if (tagInfo.icon != null && tagInfo.staticEntry != null)
return true;
}
return false;
}
public void applyChanges(@NonNull Context context) {
TagsHandler tagsHandler = TBApplication.tagsHandler(context);
IconsHandler iconsHandler = TBApplication.iconsHandler(context);
TBLauncherActivity launcherActivity = TBApplication.launcherActivity(context);
boolean changesMade = false;
for (TagInfo tagInfo : mTagList) {
if (tagInfo.staticEntry instanceof ActionEntry) {
// can't delete actions (it's the show untagged action)
if (tagInfo.action == TagInfo.Action.RENAME) {
TBApplication.dataHandler(context).renameStaticEntry(tagInfo.staticEntry, tagInfo.name);
changesMade = true;
}
} else {
switch (tagInfo.action) {
case RENAME:
if (tagsHandler.renameTag(tagInfo.tagName, tagInfo.name))
changesMade = true;
break;
case DELETE:
if (tagInfo.staticEntry != null)
iconsHandler.restoreDefaultIcon(tagInfo.staticEntry);
tagInfo.icon = null;
if (tagsHandler.removeTag(tagInfo.tagName))
changesMade = true;
break;
}
}
if (tagInfo.icon != null && tagInfo.staticEntry != null) {
iconsHandler.changeIcon(tagInfo.staticEntry, tagInfo.icon);
if (launcherActivity != null) {
// force a result refresh to update the icon in the view
launcherActivity.behaviour.refreshSearchRecord(tagInfo.staticEntry);
}
}
}
// make sure we're in sync
if (changesMade) {
if (launcherActivity != null)
launcherActivity.queueDockReload();
afterChangesMade(context);
}
}
public static void afterChangesMade(@NonNull Context context) {
TBApplication.drawableCache(context).clearCache();
DataHandler dataHandler = TBApplication.dataHandler(context);
dataHandler.reloadProviders(IProvider.LOAD_STEP_2);
RunnableTask afterProviders = TaskRunner.newTask(task -> {
TBApplication app = TBApplication.getApplication(context);
AppsHandler.setTagsForApps(app.appsHandler().getAllApps(), app.tagsHandler());
},
task -> {
Log.d(TAG, "tags and fav providers should have loaded by now");
TBLauncherActivity activity = TBApplication.launcherActivity(context);
if (activity != null) {
activity.refreshSearchRecords();
activity.queueDockReload();
}
});
DataHandler.EXECUTOR_PROVIDERS.execute(afterProviders);
}
public void bindView(@NonNull View view, @Nullable OnItemClickListener listener) {
final Context context = view.getContext();
mListView = view.findViewById(android.R.id.list);
if (listener != null) {
mListView.setOnItemClickListener((parent, v, pos, id) -> {
Object objItem = parent.getAdapter().getItem(pos);
if (objItem instanceof TagInfo) {
listener.onItemClickListener(v, (TagInfo) objItem);
}
});
}
// prepare the grid with all the tags
mAdapter = new TagsAdapter(mTagList);
mAdapter.setOnRemoveListener((adapter, v, position) -> {
TagInfo info = adapter.getItem(position);
if (info.action == TagInfo.Action.DELETE) {
info.action = info.tagName.equals(info.name) ? TagInfo.Action.NONE : TagInfo.Action.RENAME;
} else {
info.action = TagInfo.Action.DELETE;
}
mAdapter.notifyDataSetChanged();
});
mAdapter.setOnRenameListener((adapter, v, position) -> {
TagInfo info = adapter.getItem(position);
launchRenameDialog(v.getContext(), info);
});
mAdapter.setOnEditIconListener((adapter, v, position) -> {
TagInfo info = adapter.getItem(position);
launchCustomTagIconDialog(v.getContext(), info);
});
mAdapter.newLoadAsyncList(() -> {
Activity activity = Utilities.getActivity(context);
if (activity == null)
return null;
TagsHandler tagsHandler = TBApplication.tagsHandler(activity);
TagsProvider tagsProvider = TBApplication.dataHandler(activity).getTagsProvider();
Collection validTags = tagsHandler.getValidTags();
ArrayList tags = new ArrayList<>(validTags.size() + 1);
for (String tagName : validTags) {
TagEntry tagEntry = tagsProvider != null ? tagsProvider.getTagEntry(tagName) : null;
TagInfo tagInfo = tagEntry != null ? new TagInfo(tagEntry) : new TagInfo(tagName);
tagInfo.setInfo(tagName, tagsHandler.getValidEntryIds(tagName).size());
tags.add(tagInfo);
}
Collections.sort(tags, Comparator.comparing(lhs -> lhs.tagName));
EntryItem untaggedEntry = TBApplication.dataHandler(context).getPojo(ActionEntry.SCHEME + "show/untagged");
if (untaggedEntry instanceof ActionEntry) {
TagInfo tagInfo = new TagInfo((ActionEntry) untaggedEntry);
tagInfo.setInfo(untaggedEntry.getName(), -1);
tags.add(0, tagInfo);
}
return tags;
}).execute();
}
private void launchRenameDialog(Context ctx, TagInfo info) {
DialogHelper.makeRenameDialog(ctx, info.name, (dialog, newName) -> {
boolean isValid = true;
for (TagInfo tagInfo : mTagList) {
if (tagInfo == info)
continue;
if (tagInfo.tagName.equals(newName) || tagInfo.name.equals(newName)) {
isValid = false;
break;
}
}
if (!isValid) {
Toast.makeText(ctx, ctx.getString(R.string.invalid_rename_tag, newName), Toast.LENGTH_LONG).show();
return;
}
// Set new name
info.name = newName;
info.action = info.tagName.equals(info.name) ? TagInfo.Action.NONE : TagInfo.Action.RENAME;
mAdapter.notifyDataSetChanged();
})
.setTitle(R.string.title_rename_tag)
.setHint(R.string.hint_rename_tag)
.show();
}
private void launchCustomTagIconDialog(Context ctx, TagInfo info) {
DataHandler dh = TBApplication.dataHandler(ctx);
final StaticEntry staticEntry;
if (info.staticEntry != null) {
staticEntry = info.staticEntry;
} else {
TagsProvider tagsProvider = dh.getTagsProvider();
if (tagsProvider == null)
return;
TagEntry tagEntry = tagsProvider.getTagEntry(info.tagName);
// add this tag to the provider before launchCustomIconDialog, in case it isn't already
tagsProvider.addTagEntry(tagEntry);
staticEntry = tagEntry;
}
// make sure we have the tag in the provider or else IconSelectDialog will not find it
if (staticEntry instanceof TagEntry) {
TagsProvider tagsProvider = dh.getTagsProvider();
if (tagsProvider != null)
tagsProvider.addTagEntry((TagEntry) staticEntry);
}
IconSelectDialog dlg = Behaviour.getCustomIconDialog(ctx, false);
dlg.putArgString("entryId", staticEntry.id);
dlg.setOnConfirmListener(drawable -> {
int pos = mTagList.indexOf(info);
if (pos == -1)
return;
// update tag info
if (staticEntry.hasCustomIcon() && drawable == null)
info.icon = staticEntry.getDefaultDrawable(ctx);
else
info.icon = drawable;
mAdapter.notifyDataSetChanged();
});
Behaviour.showDialog(ctx, dlg, Behaviour.DIALOG_CUSTOM_ICON);
}
public void onStart() {
// Set list adapter after the view inflated
// This is a workaround to fix listview items not having the correct width
mListView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
mListView.getViewTreeObserver().removeOnPreDrawListener(this);
mListView.setAdapter(mAdapter);
return false;
}
});
//mListView.post(() -> mListView.setAdapter(mAdapter));
}
static class TagsAdapter extends ViewHolderListAdapter {
private OnItemClickListener mOnRemoveListener = null;
private OnItemClickListener mOnRenameListener = null;
private OnItemClickListener mOnEditIconListener = null;
public interface OnItemClickListener {
void onClick(TagsAdapter adapter, View view, int position);
}
TagsAdapter(@NonNull ArrayList tags) {
super(TagViewHolder.class, R.layout.tags_manager_item, tags);
}
void setOnRemoveListener(OnItemClickListener listener) {
mOnRemoveListener = listener;
}
void setOnRenameListener(OnItemClickListener listener) {
mOnRenameListener = listener;
}
void setOnEditIconListener(OnItemClickListener listener) {
mOnEditIconListener = listener;
}
@Override
protected int getItemViewTypeLayout(int viewType) {
if (viewType == 1)
return R.layout.tags_manager_item_deleted;
return super.getItemViewTypeLayout(viewType);
}
public int getItemViewType(int position) {
return getItem(position).action == TagInfo.Action.DELETE ? 1 : 0;
}
public int getViewTypeCount() {
return 2;
}
}
public static class TagViewHolder extends ViewHolderAdapter.ViewHolder {
ImageView iconView;
TextView text1View;
TextView text2View;
View removeBtnView;
View renameBtnView;
View changeIconBtnView;
public TagViewHolder(View itemView) {
super(itemView);
iconView = itemView.findViewById(android.R.id.icon);
text1View = itemView.findViewById(android.R.id.text1);
text2View = itemView.findViewById(android.R.id.text2);
removeBtnView = itemView.findViewById(android.R.id.button1);
renameBtnView = itemView.findViewById(android.R.id.button2);
changeIconBtnView = itemView.findViewById(android.R.id.button3);
}
@Override
protected void setContent(TagInfo content, int position, @NonNull ViewHolderAdapter> adapter) {
TagsAdapter tagsAdapter = (TagsAdapter) adapter;
text1View.setText(content.name);
text1View.setTypeface(null, content.action == TagInfo.Action.RENAME ? Typeface.BOLD : Typeface.NORMAL);
if (content.staticEntry instanceof ActionEntry) {
// this is the untagged entry
removeBtnView.setVisibility(View.GONE);
Context context = text1View.getContext();
Drawable untagged = AppCompatResources.getDrawable(context, R.drawable.ic_untagged);
if (untagged != null) {
int iconSize = text1View.getHeight();
if (iconSize <= 0)
iconSize = context.getResources().getDimensionPixelSize(R.dimen.icon_preview_size);
untagged.setBounds(0, 0, iconSize, iconSize);
int dir = context.getResources().getConfiguration().getLayoutDirection();
CharSequence text = Utilities.addDrawableAfterString(content.name, untagged, dir);
text1View.setText(text);
}
} else {
removeBtnView.setVisibility(View.VISIBLE);
removeBtnView.setOnClickListener(v -> {
if (tagsAdapter.mOnRemoveListener != null)
tagsAdapter.mOnRemoveListener.onClick(tagsAdapter, v, position);
});
}
if (content.action == TagInfo.Action.DELETE) {
text1View.setPaintFlags(text1View.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
// the rest of the views are null, exit now
return;
}
int count = content.entryCount;
if (count >= 0) {
text2View.setVisibility(View.VISIBLE);
text2View.setText(text2View.getResources().getQuantityString(R.plurals.tag_entry_count, count, count));
} else {
// we can't have a negative count
text2View.setVisibility(View.GONE);
}
if (content.icon == null) {
if (content.staticEntry != null) {
int drawFlags = EntryItem.FLAG_DRAW_ICON | EntryItem.FLAG_DRAW_NO_CACHE;
ResultViewHelper.setIconAsync(drawFlags, content.staticEntry, iconView, StaticEntry.AsyncSetEntryIcon.class, StaticEntry.class);
} else {
Drawable icon = new CodePointDrawable(content.name);
icon = DrawableUtils.applyIconMaskShape(iconView.getContext(), icon, DrawableUtils.SHAPE_SQUIRCLE, false);
iconView.setImageDrawable(icon);
}
} else {
iconView.setImageDrawable(content.icon);
}
renameBtnView.setOnClickListener(v -> {
if (tagsAdapter.mOnRenameListener != null)
tagsAdapter.mOnRenameListener.onClick(tagsAdapter, v, position);
});
changeIconBtnView.setOnClickListener(v -> {
if (tagsAdapter.mOnEditIconListener != null)
tagsAdapter.mOnEditIconListener.onClick(tagsAdapter, v, position);
});
}
}
public static class TagInfo {
public final StaticEntry staticEntry;
public final String tagName;
private String name;
private Drawable icon = null;
private int entryCount;
private Action action = Action.NONE;
public enum Action {NONE, DELETE, RENAME}
public TagInfo(String name) {
staticEntry = null;
tagName = name;
}
public TagInfo(StaticEntry entry) {
staticEntry = entry;
tagName = entry.getName();
}
public void setInfo(String name, int count) {
this.name = name;
this.entryCount = count;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
TagInfo tagInfo = (TagInfo) o;
return Objects.equals(staticEntry, tagInfo.staticEntry) &&
Objects.equals(tagName, tagInfo.tagName) &&
Objects.equals(name, tagInfo.name) &&
Objects.equals(icon, tagInfo.icon) &&
action == tagInfo.action;
}
@Override
public int hashCode() {
return Objects.hash(staticEntry, tagName, name, icon, action);
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/WallpaperSnapAnim.java
================================================
package rocks.tbog.tblauncher;
import android.graphics.Point;
import android.graphics.PointF;
import android.view.VelocityTracker;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Transformation;
import androidx.annotation.Nullable;
class WallpaperSnapAnim extends Animation {
private final LiveWallpaper liveWallpaper;
final PointF mStartOffset = new PointF();
final PointF mDeltaOffset = new PointF();
final PointF mVelocity = new PointF();
WallpaperSnapAnim(LiveWallpaper liveWallpaper) {
super();
this.liveWallpaper = liveWallpaper;
setDuration(500);
setInterpolator(new DecelerateInterpolator());
}
boolean init(@Nullable VelocityTracker velocityTracker) {
if (velocityTracker == null) {
mVelocity.set(0.f, 0.f);
} else {
mVelocity.set(velocityTracker.getXVelocity(), velocityTracker.getYVelocity());
}
//Log.d(TAG, "mVelocity=" + String.format(Locale.US, "%.2f", mVelocity));
mStartOffset.set(liveWallpaper.getWallpaperOffset());
//Log.d(TAG, "mStartOffset=" + String.format(Locale.US, "%.2f", mStartOffset));
final Point windowSize = liveWallpaper.getWindowSize();
final float expectedPosX = -Math.min(Math.max(mVelocity.x / windowSize.x, -.5f), .5f) + mStartOffset.x;
final float expectedPosY = -Math.min(Math.max(mVelocity.y / windowSize.y, -.5f), .5f) + mStartOffset.y;
//Log.d(TAG, "expectedPos=" + String.format(Locale.US, "%.2f %.2f", expectedPosX, expectedPosY));
SnapInfo si = new SnapInfo(liveWallpaper.isPreferenceWPStickToSides(), liveWallpaper.isPreferenceWPReturnCenter());
si.init(expectedPosX, expectedPosY);
si.removeDiagonals(expectedPosX, expectedPosY);
// compute offset based on stick location
if (si.stickToTop)
mDeltaOffset.y = 0.f - mStartOffset.y;
else if (si.stickToBottom)
mDeltaOffset.y = 1.f - mStartOffset.y;
else if (si.stickToCenter)
mDeltaOffset.y = .5f - mStartOffset.y;
if (si.stickToLeft)
mDeltaOffset.x = 0.f - mStartOffset.x;
else if (si.stickToRight)
mDeltaOffset.x = 1.f - mStartOffset.x;
else if (si.stickToCenter)
mDeltaOffset.x = .5f - mStartOffset.x;
return si.stickToLeft || si.stickToTop || si.stickToRight || si.stickToBottom || si.stickToCenter;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float offsetX = mStartOffset.x + mDeltaOffset.x * interpolatedTime;
float offsetY = mStartOffset.y + mDeltaOffset.y * interpolatedTime;
float velocityInterpolator = (float) Math.sqrt(interpolatedTime) * 3.f;
final Point windowSize = liveWallpaper.getWindowSize();
if (velocityInterpolator < 1.f) {
offsetX -= mVelocity.x / windowSize.x * velocityInterpolator;
offsetY -= mVelocity.y / windowSize.y * velocityInterpolator;
} else {
offsetX -= mVelocity.x / windowSize.x * (1.f - 0.5f * (velocityInterpolator - 1.f));
offsetY -= mVelocity.y / windowSize.y * (1.f - 0.5f * (velocityInterpolator - 1.f));
}
liveWallpaper.updateWallpaperOffset(offsetX, offsetY);
}
private static class SnapInfo {
public final boolean stickToSides;
public final boolean stickToCenter;
public boolean stickToLeft;
public boolean stickToTop;
public boolean stickToRight;
public boolean stickToBottom;
public SnapInfo(boolean sidesSnap, boolean centerSnap) {
stickToSides = sidesSnap;
stickToCenter = centerSnap;
}
public void init(float x, float y) {
// if we stick only to the center
float leftStickPercent = -1.f;
float topStickPercent = -1.f;
float rightStickPercent = 2.f;
float bottomStickPercent = 2.f;
if (stickToSides && stickToCenter) {
// if we stick to the left, right and center
leftStickPercent = .2f;
topStickPercent = .2f;
rightStickPercent = .8f;
bottomStickPercent = .8f;
} else if (stickToSides) {
// if we stick only to the center
leftStickPercent = .5f;
topStickPercent = .5f;
rightStickPercent = .5f;
bottomStickPercent = .5f;
}
stickToLeft = x <= leftStickPercent;
stickToTop = y <= topStickPercent;
stickToRight = x >= rightStickPercent;
stickToBottom = y >= bottomStickPercent;
}
public void removeDiagonals(float x, float y) {
if (stickToTop) {
// don't stick to the top-left or top-right corner
if (stickToLeft) {
stickToLeft = x < y;
stickToTop = !stickToLeft;
} else if (stickToRight) {
stickToRight = (1.f - x) < y;
stickToTop = !stickToRight;
}
} else if (stickToBottom) {
// don't stick to the bottom-left or bottom-right corner
if (stickToLeft) {
stickToLeft = x < y;
stickToBottom = !stickToLeft;
} else if (stickToRight) {
stickToRight = (1.f - x) < y;
stickToBottom = !stickToRight;
}
}
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/WorkAsync/AsyncTask.java
================================================
package rocks.tbog.tblauncher.WorkAsync;
import android.util.Log;
import androidx.annotation.MainThread;
import androidx.annotation.WorkerThread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public abstract class AsyncTask extends FutureTask {
private static final String TAG = "AsyncT";
In input = null;
protected AsyncTask() {
this(new BackgroundWorker<>());
}
private AsyncTask(BackgroundWorker worker) {
super(worker);
worker.task = this;
}
@MainThread
protected void onPreExecute() {
}
@WorkerThread
protected abstract Out doInBackground(In input);
@Override
protected void done() {
TaskRunner.runOnUiThread(() -> {
if (isCancelled())
onCancelled();
else {
Out result = null;
try {
result = get();
} catch (ExecutionException | InterruptedException e) {
Log.e(TAG, "AsyncTask " + AsyncTask.this, e);
}
onPostExecute(result);
}
});
}
@MainThread
protected void onPostExecute(Out output) {
}
@MainThread
protected void onCancelled() {
}
private static class BackgroundWorker implements Callable {
private AsyncTask task = null;
@Override
public Out call() {
Out output = task.doInBackground(task.input);
task.input = null;
return output;
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/WorkAsync/RunnableTask.java
================================================
package rocks.tbog.tblauncher.WorkAsync;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public final class RunnableTask extends FutureTask {
private TaskRunner.AsyncRunnable whenDone = null;
private Lifecycle lifecycle = null;
public void cancel() {
cancel(false);
}
protected RunnableTask(@NonNull TaskRunner.AsyncRunnable worker, @Nullable TaskRunner.AsyncRunnable main, @Nullable Lifecycle lifecycle) {
this(new BackgroundWorker(worker));
whenDone = main;
this.lifecycle = lifecycle;
}
protected RunnableTask(@NonNull TaskRunner.AsyncRunnable worker, @Nullable TaskRunner.AsyncRunnable main) {
this(worker, main, null);
}
private RunnableTask(@NonNull BackgroundWorker background) {
super(background);
background.task = this;
}
@Override
protected void done() {
if (whenDone != null) {
TaskRunner.runOnUiThread(() -> {
if (lifecycle == null || lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED))
whenDone.run(this);
});
}
}
private static class BackgroundWorker implements Callable {
private RunnableTask task = null;
private final TaskRunner.AsyncRunnable worker;
private BackgroundWorker(TaskRunner.AsyncRunnable worker) {
this.worker = worker;
}
@Override
public RunnableTask call() {
worker.run(task);
return task;
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/WorkAsync/TaskRunner.java
================================================
package rocks.tbog.tblauncher.WorkAsync;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import java.util.concurrent.ExecutorService;
public class TaskRunner {
private final static Handler handler = new Handler(Looper.getMainLooper());
public interface AsyncRunnable {
void run(@NonNull RunnableTask task);
}
public static boolean runOnUiThread(Runnable runnable) {
return handler.post(runnable);
}
@NonNull
public static RunnableTask newTask(@NonNull Lifecycle lifecycle, @NonNull AsyncRunnable worker, @Nullable AsyncRunnable main) {
return new RunnableTask(worker, main, lifecycle);
}
@NonNull
public static RunnableTask newTask(@NonNull AsyncRunnable worker, @Nullable AsyncRunnable main) {
return new RunnableTask(worker, main);
}
@MainThread
public static > void executeOnExecutor(@NonNull ExecutorService executor, @NonNull T task) {
executeOnExecutor(executor, task, null);
}
@MainThread
public static void executeOnExecutor(@NonNull ExecutorService executor, @NonNull AsyncTask task, @Nullable In input) {
task.onPreExecute();
task.input = input;
executor.submit(task);
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/broadcast/IncomingCallHandler.java
================================================
package rocks.tbog.tblauncher.broadcast;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telephony.TelephonyManager;
import android.util.Log;
import rocks.tbog.tblauncher.handler.DataHandler;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.dataprovider.ContactsProvider;
import rocks.tbog.tblauncher.entry.ContactEntry;
public class IncomingCallHandler extends BroadcastReceiver {
private static final String TAG = IncomingCallHandler.class.getSimpleName();
@Override
public void onReceive(final Context context, Intent intent) {
// Only handle calls received
if (!"android.intent.action.PHONE_STATE".equals(intent.getAction())) {
return;
}
try {
DataHandler dataHandler = TBApplication.getApplication(context).getDataHandler();
ContactsProvider contactsProvider = dataHandler.getContactsProvider();
// Stop if contacts are not enabled
if (contactsProvider == null) {
return;
}
if (TelephonyManager.EXTRA_STATE_RINGING.equals(intent.getStringExtra(TelephonyManager.EXTRA_STATE))) {
String phoneNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
if (phoneNumber == null) {
// Skipping (private call)
return;
}
ContactEntry contactEntry = contactsProvider.findByPhone(phoneNumber);
if (contactEntry != null) {
dataHandler.addToHistory(contactEntry.getHistoryId());
}
}
} catch (Exception e) {
Log.e(TAG, "Phone Receive Error", e);
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/broadcast/LocaleChangedReceiver.java
================================================
package rocks.tbog.tblauncher.broadcast;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.dataprovider.AppProvider;
public class LocaleChangedReceiver extends BroadcastReceiver {
@Override
@SuppressWarnings("CatchAndPrintStackTrace")
public void onReceive(Context ctx, Intent intent) {
// Only handle system broadcasts
if (!"android.intent.action.LOCALE_CHANGED".equals(intent.getAction())) {
return;
}
try {
// If new locale, then reset tags to load the correct aliases
TBApplication.tagsHandler(ctx).loadFromDB(true);
} catch (IllegalStateException e) {
// Since Android 8.1, we're not allowed to create a new service
// when the app is not running
e.printStackTrace();
}
// Reload application list
final AppProvider provider = TBApplication.getApplication(ctx).getDataHandler().getAppProvider();
if (provider != null) {
provider.reload(true);
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/broadcast/PackageAddedRemovedHandler.java
================================================
package rocks.tbog.tblauncher.broadcast;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import androidx.preference.PreferenceManager;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.TBLauncherActivity;
import rocks.tbog.tblauncher.dataprovider.AppProvider;
import rocks.tbog.tblauncher.dataprovider.ShortcutsProvider;
import rocks.tbog.tblauncher.entry.AppEntry;
import rocks.tbog.tblauncher.handler.DataHandler;
import rocks.tbog.tblauncher.utils.UserHandleCompat;
/**
* This class gets called when an application is created or removed on the
* system
*
* We then recreate our data set.
*
* @author dorvaryn
*/
public class PackageAddedRemovedHandler extends BroadcastReceiver {
public static void handleEvent(Context ctx, String action, String packageName, UserHandleCompat user, boolean replacing) {
DataHandler dataHandler = TBApplication.getApplication(ctx).getDataHandler();
Log.i("Pack", action + " " + packageName + " isCurrentUser:" + user.isCurrentUser());
if (PreferenceManager.getDefaultSharedPreferences(ctx).getBoolean("enable-app-history", true)) {
// Insert into history new packages (not updated ones)
if (Intent.ACTION_PACKAGE_ADDED.equals(action) && !replacing) {
// Add new package to history
Intent launchIntent = ctx.getPackageManager().getLaunchIntentForPackage(packageName);
if (launchIntent == null || launchIntent.getComponent() == null) {
//for some plugin app
return;
}
final String appId = AppEntry.generateAppId(launchIntent.getComponent(), user);
dataHandler.addToHistory(appId);
}
}
if ("android.intent.action.PACKAGE_REMOVED".equals(action) && !replacing) {
// Remove all installed shortcuts
dataHandler.removeShortcuts(packageName);
TBLauncherActivity launcherActivity = TBApplication.launcherActivity(ctx);
if (launcherActivity != null)
launcherActivity.behaviour.handleRemoveApp(packageName);
// dataHandler.removeFromExcluded(packageName);
}
// This may be an icon pack, reload packs
TBApplication.getApplication(ctx).resetIconsHandler();
// Reload application list
{
final AppProvider provider = dataHandler.getAppProvider();
if (provider != null)
provider.reload(true);
}
// Reload shortcuts list
{
final ShortcutsProvider provider = dataHandler.getShortcutsProvider();
if (provider != null)
provider.reload(true);
}
}
@Override
public void onReceive(Context ctx, Intent intent) {
String packageName = intent.getData() != null ? intent.getData().getSchemeSpecificPart() : null;
if (packageName == null || packageName.equalsIgnoreCase(ctx.getPackageName())) {
// When running locally, sending a new version of the APK immediately triggers a "package removed"
// There is no need to handle this event.
// Discarding it makes startup time much faster locally as apps don't have to be loaded twice.
return;
}
handleEvent(ctx,
intent.getAction(),
packageName, UserHandleCompat.CURRENT_USER,
intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
);
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/calculator/Calculator.java
================================================
package rocks.tbog.tblauncher.calculator;
import android.os.Build;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.ArrayDeque;
public class Calculator {
public static Result calculateExpression(ArrayDeque expression) {
try {
return calculateExpressionThrowing(expression);
} catch (ArithmeticException e) {
return Result.arithmeticalError();
}
}
// Implements the Shunting Yard algorithm
// https://en.wikipedia.org/wiki/Shunting-yard_algorithm
private static Result calculateExpressionThrowing(ArrayDeque expression)
throws ArithmeticException {
ArrayDeque stack = new ArrayDeque<>();
for (Tokenizer.Token token : expression) {
BigDecimal operand2 = null;
BigDecimal operand1 = null;
switch (token.type) {
case Tokenizer.Token.NUMBER_TOKEN:
stack.push(token.number);
break;
case Tokenizer.Token.UNARY_PLUS_TOKEN:
if (errorInExpression(true, stack)) {
return Result.syntacticalError();
}
//redundant: stack.push(stack.pop());
break;
case Tokenizer.Token.UNARY_MINUS_TOKEN:
if (errorInExpression(true, stack)) {
return Result.syntacticalError();
}
stack.push(stack.pop().negate());
break;
case Tokenizer.Token.SUM_TOKEN:
if (errorInExpression(false, stack)) {
return Result.syntacticalError();
}
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1.add(operand2));
break;
case Tokenizer.Token.SUBTRACT_TOKEN:
if (errorInExpression(false, stack)) {
return Result.syntacticalError();
}
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1.subtract(operand2));
break;
case Tokenizer.Token.MULTIPLY_TOKEN:
if (errorInExpression(false, stack)) {
return Result.syntacticalError();
}
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1.multiply(operand2));
break;
case Tokenizer.Token.DIVIDE_TOKEN:
if (errorInExpression(false, stack)) {
return Result.syntacticalError();
}
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1.divide(operand2, MathContext.DECIMAL32));
break;
case Tokenizer.Token.EXP_TOKEN:
if (errorInExpression(false, stack)) {
return Result.syntacticalError();
}
operand2 = stack.pop();
operand1 = stack.pop();
double pow = StrictMath.pow(operand1.doubleValue(), operand2.doubleValue());
if(!isFinite(pow)) {
throw new ArithmeticException("Not finite result: "
+ operand1.toString() + "^" + operand2.toString() + " = " + pow);
}
stack.push(new BigDecimal(pow));
break;
}
}
if(stack.size() != 1) {
return Result.syntacticalError();
}
return Result.result(stack.pop());
}
private static boolean errorInExpression(boolean isUnary, final ArrayDeque stack) {
boolean error = false;
if(isUnary) {
error = error || stack.size() < 1;
} else {
error = error || stack.size() < 2;
}
return error;
}
/**
* Returns {@code true} if the argument is a finite floating-point
* value; returns {@code false} otherwise (for NaN and infinity
* arguments).
*
* @param d the {@code double} value to be tested
* @return {@code true} if the argument is a finite
* floating-point value, {@code false} otherwise.
*/
public static boolean isFinite(double d) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Double.isFinite(d);
} else {
return Math.abs(d) <= Double.MAX_VALUE;
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/calculator/Result.java
================================================
package rocks.tbog.tblauncher.calculator;
import androidx.annotation.NonNull;
public final class Result {
static Result syntacticalError() {
return new Result<>(true);
}
static Result arithmeticalError() {
return new Result<>(false);
}
static Result result(@NonNull T result) {
return new Result<>(result);
}
public final T result;
public final boolean syntacticalError;
public final boolean arithmeticalError;
private Result(boolean isSyntactical) {
this.syntacticalError = isSyntactical;
this.arithmeticalError = !isSyntactical;
this.result = null;
}
private Result(@NonNull T result) {
this.result = result;
this.syntacticalError = this.arithmeticalError = false;
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/calculator/ShuntingYard.java
================================================
package rocks.tbog.tblauncher.calculator;
import java.util.ArrayDeque;
public class ShuntingYard {
public static Result> infixToPostfix(ArrayDeque infix) {
ArrayDeque outQueue = new ArrayDeque<>();
ArrayDeque operatorStack = new ArrayDeque<>();
for (Tokenizer.Token token : infix) {
switch (token.type) {
case Tokenizer.Token.NUMBER_TOKEN:
outQueue.add(token);
break;
case Tokenizer.Token.UNARY_PLUS_TOKEN:
case Tokenizer.Token.UNARY_MINUS_TOKEN:
case Tokenizer.Token.SUM_TOKEN:
case Tokenizer.Token.SUBTRACT_TOKEN:
case Tokenizer.Token.MULTIPLY_TOKEN:
case Tokenizer.Token.DIVIDE_TOKEN:
case Tokenizer.Token.EXP_TOKEN:
if (operatorStack.isEmpty()) {
operatorStack.push(token);
} else {
while (!operatorStack.isEmpty()) {
int prec1 = token.getPrecedence();
int prec2 = operatorStack.peek().getPrecedence();
if ((token.isLeftAssociative() && prec1 <= prec2) || (token.isRightAssociative() && prec1 < prec2)) {
outQueue.add(operatorStack.pop());
} else {
break;
}
}
operatorStack.push(token);
}
break;
case Tokenizer.Token.PARENTHESIS_OPEN_TOKEN:
operatorStack.push(token);
break;
case Tokenizer.Token.PARENTHESIS_CLOSE_TOKEN:
if(operatorStack.isEmpty()) {
return Result.syntacticalError();
}
// until '(' on stack, pop operators.
while (operatorStack.peek().type != Tokenizer.Token.PARENTHESIS_OPEN_TOKEN) {
outQueue.add(operatorStack.pop());
if(operatorStack.isEmpty()) {
return Result.syntacticalError();
}
}
operatorStack.pop();
break;
}
}
while (!operatorStack.isEmpty()) {
outQueue.addLast(operatorStack.pop());
}
return Result.result(outQueue);
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/calculator/Tokenizer.java
================================================
package rocks.tbog.tblauncher.calculator;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import java.text.ParseException;
import java.util.ArrayDeque;
import androidx.annotation.NonNull;
public class Tokenizer {
public static final class Token {
public static final int SUM_TOKEN = 0;
public static final int SUBTRACT_TOKEN = 1;
public static final int MULTIPLY_TOKEN = 2;
public static final int DIVIDE_TOKEN = 3;
public static final int EXP_TOKEN = 4;
public static final int UNARY_PLUS_TOKEN = 8;
public static final int UNARY_MINUS_TOKEN = 9;
public static final int NUMBER_TOKEN = 16;
public static final int PARENTHESIS_OPEN_TOKEN = 17;
public static final int PARENTHESIS_CLOSE_TOKEN = 18;
public final int type;
public final BigDecimal number;
public Token(int type) {
if(type != SUM_TOKEN && type != SUBTRACT_TOKEN && type != MULTIPLY_TOKEN && type != DIVIDE_TOKEN
&& type != EXP_TOKEN
&& type != UNARY_PLUS_TOKEN && type != UNARY_MINUS_TOKEN) {
throw new IllegalArgumentException("Wrong constructor!");
}
this.type = type;
number = null;
}
public Token(@NonNull BigDecimal number) {
this.type = NUMBER_TOKEN;
this.number = number;
}
public Token(boolean isParenthesisOpen) {
this.type = isParenthesisOpen? PARENTHESIS_OPEN_TOKEN : PARENTHESIS_CLOSE_TOKEN;
this.number = null;
}
public final int getPrecedence() {
switch (type) {
case UNARY_PLUS_TOKEN:
case UNARY_MINUS_TOKEN:
return 4;
case SUM_TOKEN:
case SUBTRACT_TOKEN:
return 1;
case MULTIPLY_TOKEN:
case DIVIDE_TOKEN:
return 2;
case EXP_TOKEN:
return 3;
default:
return -1;
}
}
public final boolean isRightAssociative() {
switch (type) {
case UNARY_PLUS_TOKEN:
case UNARY_MINUS_TOKEN:
return true;
case SUM_TOKEN:
case SUBTRACT_TOKEN:
case MULTIPLY_TOKEN:
case DIVIDE_TOKEN:
case EXP_TOKEN:
return false;
default:
throw new IllegalStateException();
}
}
public final boolean isLeftAssociative() {
return !isRightAssociative();
}
}
public static Result> tokenize(String expression) {
ArrayDeque tokens = new ArrayDeque<>();
for (int i = 0; i < expression.length(); i++) {
char operator = expression.charAt(i);
Token token = null;
switch (operator) {
case '+':
if(!tokens.isEmpty() && (tokens.peekLast().type == Token.NUMBER_TOKEN
|| tokens.peekLast().type == Token.PARENTHESIS_CLOSE_TOKEN)) {
token = new Token(Token.SUM_TOKEN);
} else {
token = new Token(Token.UNARY_PLUS_TOKEN);
}
break;
case '-':
if(!tokens.isEmpty() && (tokens.peekLast().type == Token.NUMBER_TOKEN
|| tokens.peekLast().type == Token.PARENTHESIS_CLOSE_TOKEN)) {
token = new Token(Token.SUBTRACT_TOKEN);
} else {
token = new Token(Token.UNARY_MINUS_TOKEN);
}
break;
case '*':
if(expression.length() > i+1 && expression.charAt(i+1) == '*') {// '**'
i++;
token = new Token(Token.EXP_TOKEN);
} else {
token = new Token(Token.MULTIPLY_TOKEN);
}
break;
case '×':
case 'x':
token = new Token(Token.MULTIPLY_TOKEN);
break;
case '/':
case '÷':
token = new Token(Token.DIVIDE_TOKEN);
break;
case '^':
token = new Token(Token.EXP_TOKEN);
break;
case '(':
token = new Token(true);
break;
case ')':
token = new Token(false);
break;
default:
//Numbers
StringBuilder numberBuilder = new StringBuilder();
if(checkOperatorIsPartOfNumber(operator)) {
numberBuilder.append(operator);
while (i+1 < expression.length()) {
operator = expression.charAt(i+1);
if(checkOperatorIsPartOfNumber(operator)) {
numberBuilder.append(operator);
i++;
} else {
break;
}
}
}
if(numberBuilder.length() != 0) {
DecimalFormat decimalFormat = (DecimalFormat) DecimalFormat.getInstance();
decimalFormat.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US));
decimalFormat.setParseBigDecimal(true);
BigDecimal number = null;
String numberStr = numberBuilder.toString().replace(",",".");
if (numberStr.matches("[^.]*\\.[^.]*\\..*")) {
return Result.syntacticalError(); // multiple decimal points in one token
}
try {
number = (BigDecimal) decimalFormat.parse(numberStr);
} catch (ParseException e) {
return Result.syntacticalError();
}
token = new Token(number);
}
}
if(token == null) {
continue;
}
tokens.addLast(token);
}
return Result.result(tokens);
}
private static boolean checkOperatorIsPartOfNumber(char operator) {
return Character.isDigit(operator) || operator == '.' || operator == ',' || operator == 'E'
|| operator == ' ' || operator == '\'';
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/customicon/ButtonHelper.java
================================================
package rocks.tbog.tblauncher.customicon;
import android.content.Context;
import android.view.View;
import androidx.annotation.NonNull;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.ui.ListPopup;
public class ButtonHelper {
public static final String BTN_ID_PHONE = "button://item_contact_action_phone";
public static final String BTN_ID_MESSAGE = "button://item_contact_action_message";
public static final String BTN_ID_OPEN = "button://item_contact_action_open";
public static final String BTN_ID_LAUNCHER_PILL = "button://launcher_pill";
public static final String BTN_ID_LAUNCHER_WHITE = "button://launcher_white";
private ButtonHelper() {
// don't instantiate a namespace
}
public static boolean showButtonPopup(@NonNull View view, @NonNull ListPopup buttonMenu) {
final Context ctx = view.getContext();
// check if menu contains elements and if yes show it
if (!buttonMenu.getAdapter().isEmpty()) {
TBApplication.getApplication(ctx).registerPopup(buttonMenu);
buttonMenu.show(view);
return true;
}
return false;
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/customicon/CustomShapePage.java
================================================
package rocks.tbog.tblauncher.customicon;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.fragment.app.DialogFragment;
import com.github.dhaval2404.imagepicker.ImagePicker;
import com.github.dhaval2404.imagepicker.constant.ImageProvider;
import net.mm2d.color.chooser.ColorChooserDialog;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import kotlin.Unit;
import rocks.tbog.tblauncher.CustomizeUI;
import rocks.tbog.tblauncher.R;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.drawable.CodePointDrawable;
import rocks.tbog.tblauncher.drawable.DrawableUtils;
import rocks.tbog.tblauncher.drawable.FourCodePointDrawable;
import rocks.tbog.tblauncher.drawable.TextDrawable;
import rocks.tbog.tblauncher.drawable.TwoCodePointDrawable;
import rocks.tbog.tblauncher.handler.IconsHandler;
import rocks.tbog.tblauncher.utils.UIColors;
import rocks.tbog.tblauncher.utils.UISizes;
import rocks.tbog.tblauncher.utils.Utilities;
import rocks.tbog.tblauncher.utils.ViewHolderAdapter;
import rocks.tbog.tblauncher.utils.ViewHolderListAdapter;
class CustomShapePage extends PageAdapter.Page {
protected ShapedIconAdapter mShapesAdapter;
protected ShapedIconAdapter mShapedIconAdapter;
protected TextView mLettersView;
protected int mShape;
protected float mScale = 1.f;
protected int mBackground;
private int mLetters;
CustomShapePage(CharSequence name, View view) {
super(name, view);
final Context ctx = view.getContext();
mShape = TBApplication.iconsHandler(ctx).getSystemIconPack().getAdaptiveShape();
mLetters = UIColors.getContactActionColor(ctx);
mBackground = UIColors.getIconBackground(ctx);
if (mShape == DrawableUtils.SHAPE_NONE)
mShape = DrawableUtils.SHAPE_SQUARE;
}
@Override
void setupView(@NonNull DialogFragment dialogFragment, @Nullable OnItemClickListener iconClickListener, @Nullable OnItemClickListener iconLongClickListener) {
Context context = dialogFragment.requireContext();
mLettersView = pageView.findViewById(R.id.letters);
mLettersView.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
generateTextIcons(s);
}
@Override
public void afterTextChanged(Editable s) {
}
});
// shape list toggle
setupToggle(R.id.shapeGridToggle, R.id.shapeGrid);
// scale bar toggle
setupToggle(R.id.scaleBarToggle, R.id.scaleBar);
// letters toggle
setupToggle(R.id.lettersToggle, R.id.lettersGroup);
addShapesList();
addIconsList(iconClickListener);
addScaleBar();
// add icon picker
{
PickedIconInfo iconInfo = new PickedIconInfo(AppCompatResources.getDrawable(context, R.drawable.ic_browse_add_icon), R.string.browse_add_icon);
mShapedIconAdapter.addItem(iconInfo);
}
final float colorPreviewRadius = dialogFragment.getResources().getDimension(R.dimen.color_preview_radius);
final int colorPreviewBorder = UISizes.dp2px(context, 1);
final int colorPreviewSize = dialogFragment.getResources().getDimensionPixelSize(R.dimen.color_preview_size);
addBackgroundColorChooser(colorPreviewRadius, colorPreviewBorder, colorPreviewSize);
addLetterColorChooser(colorPreviewRadius, colorPreviewBorder, colorPreviewSize);
generateShapes(context);
}
private void addShapesList() {
GridView shapeGridView = pageView.findViewById(R.id.shapeGrid);
mShapesAdapter = new ShapedIconAdapter();
shapeGridView.setAdapter(mShapesAdapter);
shapeGridView.setOnItemClickListener((parent, view, position, id) -> {
Activity activity = Utilities.getActivity(view);
if (activity == null)
return;
Object objItem = parent.getAdapter().getItem(position);
if (!(objItem instanceof NamedIconInfo) || ((NamedIconInfo) objItem).getPreview() == null)
return;
CharSequence name = ((NamedIconInfo) objItem).name;
for (int shape : DrawableUtils.SHAPE_LIST) {
if (name.equals(DrawableUtils.shapeName(activity, shape))) {
mShape = shape;
break;
}
}
reshapeIcons(activity);
});
CustomizeUI.setResultListPref(shapeGridView);
}
private void addIconsList(@Nullable OnItemClickListener iconClickListener) {
GridView gridView = pageView.findViewById(R.id.iconGrid);
mShapedIconAdapter = new ShapedIconAdapter();
gridView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
gridView.getViewTreeObserver().removeOnPreDrawListener(this);
gridView.setAdapter(mShapedIconAdapter);
return false;
}
});
//gridView.setAdapter(mShapedIconAdapter);
if (iconClickListener != null)
gridView.setOnItemClickListener((parent, view, position, id) -> {
Object item = parent.getAdapter().getItem(position);
if (item instanceof ShapedIconInfo && ((ShapedIconInfo) item).getPreview() != null)
iconClickListener.onItemClick(parent.getAdapter(), view, position);
});
CustomizeUI.setResultListPref(gridView);
}
private void addScaleBar() {
SeekBar seekBar = pageView.findViewById(R.id.scaleBar);
seekBar.setMax(200);
seekBar.setProgress((int) (100.f * mScale));
final Runnable updateIcons = () -> {
mScale = 0.01f * seekBar.getProgress();
reshapeIcons(seekBar.getContext());
};
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
seekBar.removeCallbacks(updateIcons);
seekBar.post(updateIcons);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
seekBar.removeCallbacks(updateIcons);
seekBar.post(updateIcons);
}
});
}
private void addBackgroundColorChooser(float colorPreviewRadius, int colorPreviewBorder, int colorPreviewSize) {
TextView colorView = pageView.findViewById(R.id.backgroundColor);
{
Drawable drawable = UIColors.getPreviewDrawable(mBackground, colorPreviewBorder, colorPreviewRadius);
drawable.setBounds(0, 0, colorPreviewSize, colorPreviewSize);
colorView.setCompoundDrawables(null, null, drawable, null);
}
colorView.setOnClickListener(v -> {
Context ctx = v.getContext();
launchCustomColorDialog(ctx, mBackground, color -> {
mBackground = color;
Activity activity = Utilities.getActivity(v);
if (activity == null)
return;
Drawable drawable = UIColors.getPreviewDrawable(mBackground, colorPreviewBorder, colorPreviewRadius);
drawable.setBounds(0, 0, colorPreviewSize, colorPreviewSize);
colorView.setCompoundDrawables(null, null, drawable, null);
generateShapes(activity);
reshapeIcons(activity);
});
});
}
private void addLetterColorChooser(float colorPreviewRadius, int colorPreviewBorder, int colorPreviewSize) {
TextView colorView = pageView.findViewById(R.id.lettersColor);
{
Drawable drawable = UIColors.getPreviewDrawable(mLetters, colorPreviewBorder, colorPreviewRadius);
drawable.setBounds(0, 0, colorPreviewSize, colorPreviewSize);
colorView.setCompoundDrawables(null, null, drawable, null);
}
colorView.setOnClickListener(v -> {
Context ctx = v.getContext();
launchCustomColorDialog(ctx, mLetters, color -> {
mLetters = color;
Activity activity = Utilities.getActivity(v);
if (activity == null)
return;
Drawable drawable = UIColors.getPreviewDrawable(mLetters, colorPreviewBorder, colorPreviewRadius);
drawable.setBounds(0, 0, colorPreviewSize, colorPreviewSize);
colorView.setCompoundDrawables(null, null, drawable, null);
generateTextIcons(mLettersView.getText());
});
});
}
@Override
public void addPickedIcon(@NonNull Drawable pickedImage, String filename) {
mShapedIconAdapter.addItem(new PickedIconInfo(pickedImage, filename));
}
private void setupToggle(@IdRes int toggleTextView, @IdRes int viewToToggle) {
TextView textView = pageView.findViewById(toggleTextView);
textView.setOnClickListener(v -> {
View view = pageView.findViewById(viewToToggle);
if ("hide".equals(v.getTag())) {
view.setVisibility(View.GONE);
((TextView) v).setCompoundDrawablesWithIntrinsicBounds(0, 0, android.R.drawable.arrow_down_float, 0);
v.setTag("show");
} else {
view.setVisibility(View.VISIBLE);
view.requestFocus();
((TextView) v).setCompoundDrawablesWithIntrinsicBounds(0, 0, android.R.drawable.arrow_up_float, 0);
v.setTag("hide");
}
});
if (textView.getTag() == null) {
textView.setTag("hide");
textView.performClick();
}
}
public void addIcon(@NonNull String name, @NonNull Drawable drawable) {
Context context = pageView.getContext();
Drawable shapedDrawable = DrawableUtils.applyIconMaskShape(context, drawable, mShape, mScale, mBackground);
NamedIconInfo iconInfo = new NamedIconInfo(name, shapedDrawable, drawable);
mShapedIconAdapter.addItem(iconInfo);
}
private void addTextIcon(CharSequence name, @NonNull TextDrawable icon) {
final Context ctx = pageView.getContext();
final ShapedIconAdapter adapter = mShapedIconAdapter;
icon.setTextColor(mLetters);
Drawable shapedIcon = DrawableUtils.applyIconMaskShape(ctx, icon, mShape, mScale, mBackground);
adapter.addItem(new LetterIconInfo(name, shapedIcon, icon));
}
private void generateTextIcons(@Nullable CharSequence text) {
final ShapedIconAdapter adapter = mShapedIconAdapter;
// remove all TextDrawable icons
for (Iterator iterator = adapter.getList().iterator(); iterator.hasNext(); ) {
ShapedIconInfo info = iterator.next();
if (info instanceof LetterIconInfo)
iterator.remove();
}
adapter.notifyDataSetChanged();
final StringBuilder name = new StringBuilder();
final int length = Utilities.codePointsLength(text);
int pos = 0;
if (length >= 1) {
name.appendCodePoint(Character.codePointAt(text, pos));
TextDrawable icon = new CodePointDrawable(text);
addTextIcon(name.toString(), icon);
}
// two characters
if (length >= 2) {
pos = Utilities.getNextCodePointIndex(text, pos);
name.appendCodePoint(Character.codePointAt(text, pos));
TextDrawable icon = TwoCodePointDrawable.fromText(text, false);
addTextIcon(name.toString(), icon);
}
if (length >= 2) {
TextDrawable icon = TwoCodePointDrawable.fromText(text, true);
addTextIcon(name.toString(), icon);
}
// three characters
if (length >= 3) {
pos = Utilities.getNextCodePointIndex(text, pos);
name.appendCodePoint(Character.codePointAt(text, pos));
TextDrawable icon = FourCodePointDrawable.fromText(text, true);
addTextIcon(name.toString(), icon);
}
// four characters
if (length >= 4) {
pos = Utilities.getNextCodePointIndex(text, pos);
name.appendCodePoint(Character.codePointAt(text, pos));
}
if (length >= 3) {
TextDrawable icon = FourCodePointDrawable.fromText(text, false);
addTextIcon(name.toString(), icon);
}
}
private void generateShapes(Context context) {
final ShapedIconAdapter adapter = mShapesAdapter;
adapter.getList().clear();
adapter.notifyDataSetChanged();
Drawable drawable = new ColorDrawable(mBackground);
for (int shape : DrawableUtils.SHAPE_LIST) {
String name = DrawableUtils.shapeName(context, shape);
Drawable shapedDrawable;
if (shape == DrawableUtils.SHAPE_NONE) {
shapedDrawable = new ColorDrawable(Color.TRANSPARENT);
} else {
shapedDrawable = DrawableUtils.applyIconMaskShape(context, drawable, shape);
}
NamedIconInfo iconInfo = new NamedIconInfo(name, shapedDrawable, null);
adapter.addItem(iconInfo);
}
}
private void reshapeIcons(Context context) {
//generateTextIcons(null);
for (ListIterator iterator = mShapedIconAdapter.getList().listIterator(); iterator.hasNext(); ) {
ShapedIconInfo iconInfo = iterator.next();
if (iconInfo.textId == R.string.icon_pack_loading)
continue;
if (iconInfo.textId == R.string.default_icon)
continue;
ShapedIconInfo newInfo = iconInfo.reshape(context, mShape, mScale, mBackground);
iterator.set(newInfo);
}
mShapedIconAdapter.notifyDataSetChanged();
//generateTextIcons(mLettersView.getText());
}
interface OnColorChanged {
void onColorChanged(int color);
}
private static void launchCustomColorDialog(@Nullable Context context, int selectedColor, @NonNull OnColorChanged listener) {
Activity activity = Utilities.getActivity(context);
if (!(activity instanceof AppCompatActivity))
return;
ColorChooserDialog.INSTANCE.registerListener((AppCompatActivity) activity, "request color", color -> {
listener.onColorChanged(color);
return Unit.INSTANCE;
}, null);
ColorChooserDialog.INSTANCE.show((AppCompatActivity) activity, "request color", selectedColor, true, ColorChooserDialog.TAB_PALETTE);
// Context themeWrapper = UITheme.getDialogThemedContext(context);
// DialogView dialogView = new DialogView(themeWrapper);
//
// dialogView.init(selectedColor, (AppCompatActivity) activity);
// dialogView.setWithAlpha(true);
//
// DialogInterface.OnClickListener buttonListener = (dialog, which) -> {
// if (which == DialogInterface.BUTTON_POSITIVE) {
// listener.onColorChanged(dialogView.getColor());
// }
// dialog.dismiss();
// };
//
// final AlertDialog.Builder builder = new AlertDialog.Builder(themeWrapper)
// .setPositiveButton(android.R.string.ok, buttonListener)
// .setNegativeButton(android.R.string.cancel, buttonListener);
// builder.setView(dialogView);
// DialogHelper.setButtonBarBackground(builder.show());
}
static class LetterIconInfo extends NamedIconInfo {
LetterIconInfo(CharSequence name, Drawable icon, Drawable text) {
super(name, icon, text);
}
@Override
protected ShapedIconInfo reshape(Context context, int shape, float scale, int background) {
Drawable drawable = DrawableUtils.applyIconMaskShape(context, originalDrawable, shape, scale, background);
return new LetterIconInfo(name, drawable, originalDrawable);
}
}
static class DefaultIconInfo extends ShapedIconInfo {
DefaultIconInfo(IconsHandler.IconInfo icon) {
super(icon.getDrawable(), icon.getDrawable());
}
@Override
public Drawable getIcon() {
return null;
}
}
static class NamedIconInfo extends ShapedIconInfo {
final CharSequence name;
NamedIconInfo(CharSequence name, Drawable icon, Drawable origin) {
super(icon, origin);
this.name = name;
}
@Override
protected ShapedIconInfo reshape(Context context, int shape, float scale, int background) {
Drawable drawable = DrawableUtils.applyIconMaskShape(context, originalDrawable, shape, scale, background);
return new NamedIconInfo(name, drawable, originalDrawable);
}
@Nullable
@Override
CharSequence getText() {
return name;
}
}
public static class ShapedIconInfo {
protected final Drawable originalDrawable;
protected final Drawable iconDrawable;
@StringRes
protected int textId;
public ShapedIconInfo(Drawable icon, Drawable origin) {
iconDrawable = icon;
originalDrawable = origin;
}
protected ShapedIconInfo reshape(Context context, int shape, float scale, int background) {
Drawable drawable = DrawableUtils.applyIconMaskShape(context, originalDrawable, shape, scale, background);
ShapedIconInfo shapedIconInfo = new ShapedIconInfo(drawable, originalDrawable);
shapedIconInfo.textId = textId;
return shapedIconInfo;
}
public Drawable getIcon() {
return iconDrawable;
}
public Drawable getPreview() {
return iconDrawable;
}
@Nullable
CharSequence getText() {
return null;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
ShapedIconInfo that = (ShapedIconInfo) o;
return Objects.equals(iconDrawable, that.iconDrawable) &&
Objects.equals(textId, that.textId);
}
@Override
public int hashCode() {
return Objects.hash(iconDrawable, textId);
}
}
public static class ShapedIconVH extends ViewHolderAdapter.ViewHolder {
View root;
ImageView icon;
TextView text1;
public ShapedIconVH(View view) {
super(view);
root = view;
icon = view.findViewById(android.R.id.icon);
text1 = view.findViewById(android.R.id.text1);
}
@Override
protected void setContent(ShapedIconInfo content, int position, @NonNull ViewHolderAdapter> adapter) {
// set icon
Drawable preview = content.getPreview();
icon.setImageDrawable(preview);
icon.setVisibility(preview == null ? View.GONE : View.VISIBLE);
if (preview instanceof Animatable)
((Animatable) preview).start();
//set text
CharSequence text = content.getText();
if (text != null)
text1.setText(text);
else if (content.textId != 0)
text1.setText(content.textId);
else
text1.setText("null");
}
}
static class ShapedIconAdapter extends ViewHolderListAdapter {
protected ShapedIconAdapter() {
super(ShapedIconVH.class, R.layout.item_grid, new ArrayList<>());
}
List getList() {
return mList;
}
void removeItem(ShapedIconInfo item) {
mList.remove(item);
notifyDataSetChanged();
}
}
public static class PickedIconInfo extends ShapedIconInfo {
@Nullable
String text = null;
public PickedIconInfo(Drawable icon, @StringRes int textId) {
super(icon, icon);
this.textId = textId;
}
public PickedIconInfo(Drawable icon, @Nullable String text) {
super(icon, icon);
this.text = text;
}
public PickedIconInfo(Drawable shaped, Drawable original, @Nullable String text) {
super(shaped, original);
this.text = text;
}
@Nullable
CharSequence getText() {
return text;
}
@Override
protected ShapedIconInfo reshape(Context context, int shape, float scale, int background) {
if (textId != 0)
return this;
Drawable drawable = DrawableUtils.applyIconMaskShape(context, originalDrawable, shape, scale, background);
return new PickedIconInfo(drawable, originalDrawable, text);
}
public boolean launchPicker(@NonNull IconSelectDialog iconSelectDialog, @NonNull View v) {
if (textId == 0)
return false;
Context ctx = v.getContext();
int size = UISizes.dp2px(ctx, R.dimen.icon_size) * 2;
ImagePicker
.with(iconSelectDialog)
.cropSquare()
.provider(ImageProvider.GALLERY)
.maxResultSize(size, size)
.saveDir(new File(ctx.getCacheDir(), "ImagePicker"))
.createIntent(intent -> {
iconSelectDialog.imagePickerResult.launch(intent);
return Unit.INSTANCE;
});
return true;
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/customicon/DefaultButtonPage.java
================================================
package rocks.tbog.tblauncher.customicon;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import rocks.tbog.tblauncher.R;
import rocks.tbog.tblauncher.drawable.DrawableUtils;
import rocks.tbog.tblauncher.handler.IconsHandler;
public class DefaultButtonPage extends CustomShapePage {
final private int mDefaultIcon;
@StringRes
final private int mDefaultName;
final private String mEntryName;
DefaultButtonPage(CharSequence name, View view, String entryName, int defaultIcon, @StringRes int defaultName) {
super(name, view);
mEntryName = entryName;
mDefaultIcon = defaultIcon;
mDefaultName = defaultName;
mScale = DrawableUtils.getScaleToFit(mShape);
}
@Override
void setupView(@NonNull DialogFragment dialogFragment, @Nullable OnItemClickListener iconClickListener, @Nullable OnItemClickListener iconLongClickListener) {
Context context = dialogFragment.requireContext();
super.setupView(dialogFragment, iconClickListener, iconLongClickListener);
final Drawable originalDrawable;
// default icon
{
originalDrawable = ContextCompat.getDrawable(context, mDefaultIcon);
IconsHandler.IconInfo iconHandlerIconInfo = new IconsHandler.IconInfo().setNonAdaptiveIcon(originalDrawable);
ShapedIconInfo iconInfo = new DefaultIconInfo(dialogFragment.getString(mDefaultName), iconHandlerIconInfo);
mShapedIconAdapter.addItem(iconInfo);
}
// customizable default icon
{
Drawable shapedDrawable = DrawableUtils.applyIconMaskShape(context, originalDrawable, mShape, mScale, mBackground);
ShapedIconInfo iconInfo = new NamedIconInfo(mEntryName, shapedDrawable, originalDrawable);
mShapedIconAdapter.addItem(iconInfo);
}
// this will call generateTextIcons
mLettersView.setText(mEntryName);
}
static class DefaultIconInfo extends CustomShapePage.DefaultIconInfo {
final String name;
DefaultIconInfo(@NonNull String name, IconsHandler.IconInfo icon) {
super(icon);
this.name = name;
textId = R.string.default_icon;
}
@Nullable
@Override
CharSequence getText() {
return name;
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/customicon/IconAdapter.java
================================================
package rocks.tbog.tblauncher.customicon;
import androidx.annotation.NonNull;
import java.util.List;
import rocks.tbog.tblauncher.R;
import rocks.tbog.tblauncher.utils.ViewHolderListAdapter;
class IconAdapter extends ViewHolderListAdapter {
IconAdapter(@NonNull List objects) {
super(IconViewHolder.class, R.layout.custom_icon_item, objects);
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/customicon/IconData.java
================================================
package rocks.tbog.tblauncher.customicon;
import android.graphics.drawable.Drawable;
import rocks.tbog.tblauncher.icons.DrawableInfo;
import rocks.tbog.tblauncher.icons.IconPackXML;
class IconData {
final DrawableInfo drawableInfo;
final IconPackXML iconPack;
IconData(IconPackXML iconPack, DrawableInfo drawableInfo) {
this.iconPack = iconPack;
this.drawableInfo = drawableInfo;
}
Drawable getIcon() {
return iconPack.getDrawable(drawableInfo);
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/customicon/IconPackPage.java
================================================
package rocks.tbog.tblauncher.customicon;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
import androidx.fragment.app.DialogFragment;
import java.util.ArrayList;
import java.util.Collection;
import rocks.tbog.tblauncher.CustomizeUI;
import rocks.tbog.tblauncher.R;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.icons.DrawableInfo;
import rocks.tbog.tblauncher.icons.IconPackXML;
import rocks.tbog.tblauncher.normalizer.StringNormalizer;
import rocks.tbog.tblauncher.utils.FuzzyScore;
import rocks.tbog.tblauncher.utils.UISizes;
import rocks.tbog.tblauncher.utils.Utilities;
class IconPackPage extends PageAdapter.Page {
private static final String TAG = IconPackPage.class.getSimpleName();
final ArrayList iconDataList = new ArrayList<>();
final String packageName;
private ProgressBar mIconLoadingBar;
private GridView mGridView;
private TextView mSearch;
private IconPackXML mIconPack = null;
private final ArraySet mInvalidDrawables = new ArraySet<>(0);
IconPackPage(CharSequence name, String packPackageName, View view) {
super(name, view);
packageName = packPackageName;
}
@Override
void setupView(@NonNull DialogFragment dialogFragment, @Nullable OnItemClickListener iconClickListener, @Nullable OnItemClickListener iconLongClickListener) {
Context context = dialogFragment.requireContext();
mIconLoadingBar = pageView.findViewById(R.id.iconLoadingBar);
Drawable packIcon = null;
// set page title
TextView textView = pageView.findViewById(android.R.id.text1);
textView.setText(dialogFragment.getResources().getString(R.string.icon_pack_content_list, packageName));
try {
packIcon = context.getPackageManager().getApplicationIcon(packageName);
} catch (PackageManager.NameNotFoundException ignored) {
}
if (packIcon != null) {
int size = UISizes.getResultIconSize(context);
packIcon.setBounds(0, 0, size, size);
textView.setCompoundDrawables(packIcon, null, null, null);
}
// set page icon grid
mGridView = pageView.findViewById(R.id.iconGrid);
IconAdapter iconAdapter = new IconAdapter(iconDataList);
mGridView.setAdapter(iconAdapter);
if (iconClickListener != null) {
mGridView.setOnItemClickListener((parent, view, position, id) -> iconClickListener.onItemClick(parent.getAdapter(), view, position));
}
if (iconLongClickListener != null) {
mGridView.setOnItemLongClickListener((parent, view, position, id) -> {
iconLongClickListener.onItemClick(parent.getAdapter(), view, position);
return true;
});
}
CustomizeUI.setResultListPref(mGridView);
// set page search bar
mSearch = pageView.findViewById(R.id.search);
mSearch.addTextChangedListener(new TextWatcher() {
public void afterTextChanged(Editable s) {
// Auto left-trim text.
if (s.length() > 0 && s.charAt(0) == ' ')
s.delete(0, 1);
}
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
mSearch.post(() -> refreshList());
}
});
mSearch.requestFocus();
// show it's loading while we parse the icon pack XML
mIconLoadingBar.setVisibility(View.VISIBLE);
mGridView.setVisibility(View.GONE);
}
@Override
void loadData() {
super.loadData();
final ArraySet invalidDrawables = new ArraySet<>(0);
// load the new pack
final IconPackXML pack = TBApplication.iconPackCache(pageView.getContext()).getIconPack(packageName);
Utilities.runAsync((t) -> {
Activity activity = Utilities.getActivity(pageView);
if (activity != null) {
pack.loadDrawables(activity.getPackageManager());
Collection drawables = pack.getDrawableList();
for (DrawableInfo info : drawables) {
if (info.getDrawableResId(pack) == 0) {
invalidDrawables.add(info.getDrawableName());
}
}
}
}, (t) -> {
Activity activity = Utilities.getActivity(pageView);
if (activity != null) {
mIconPack = pack;
mInvalidDrawables.clear();
mInvalidDrawables.addAll(invalidDrawables);
int invalidDrawablesSize = mInvalidDrawables.size();
if (invalidDrawablesSize > 0) {
Log.w(TAG, "icon pack `" + mIconPack.getPackPackageName() + "` has " + invalidDrawablesSize + " drawable(s) without resource id");
}
refreshList();
} else
mIconPack = null;
});
}
private void refreshList() {
iconDataList.clear();
if (mIconPack != null) {
Collection drawables = mIconPack.getDrawableList();
StringNormalizer.Result normalized = StringNormalizer.normalizeWithResult(mSearch.getText(), true);
FuzzyScore fuzzyScore = new FuzzyScore(normalized.codePoints);
for (DrawableInfo info : drawables) {
if (mInvalidDrawables.contains(info.getDrawableName()))
continue;
if (fuzzyScore.match(info.getDrawableName()).match)
iconDataList.add(new IconData(mIconPack, info));
}
}
mIconLoadingBar.setVisibility(View.GONE);
boolean showGridAndSearch = !iconDataList.isEmpty() || (mSearch.length() > 0);
mSearch.setVisibility(showGridAndSearch ? View.VISIBLE : View.GONE);
mGridView.setVisibility(showGridAndSearch ? View.VISIBLE : View.GONE);
((BaseAdapter) mGridView.getAdapter()).notifyDataSetChanged();
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/customicon/IconSelectDialog.java
================================================
package rocks.tbog.tblauncher.customicon;
import android.app.Activity;
import android.app.Dialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.provider.OpenableColumns;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.content.res.ResourcesCompat;
import androidx.viewpager.widget.ViewPager;
import com.github.dhaval2404.imagepicker.ImagePicker;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import rocks.tbog.tblauncher.CustomizeUI;
import rocks.tbog.tblauncher.R;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.db.DBHelper;
import rocks.tbog.tblauncher.db.ShortcutRecord;
import rocks.tbog.tblauncher.entry.EntryItem;
import rocks.tbog.tblauncher.entry.SearchEntry;
import rocks.tbog.tblauncher.entry.ShortcutEntry;
import rocks.tbog.tblauncher.entry.StaticEntry;
import rocks.tbog.tblauncher.handler.IconsHandler;
import rocks.tbog.tblauncher.icons.IconPack;
import rocks.tbog.tblauncher.result.ResultViewHelper;
import rocks.tbog.tblauncher.ui.DialogFragment;
import rocks.tbog.tblauncher.ui.DialogWrapper;
import rocks.tbog.tblauncher.ui.LinearAdapter;
import rocks.tbog.tblauncher.ui.ListPopup;
import rocks.tbog.tblauncher.utils.UISizes;
import rocks.tbog.tblauncher.utils.UserHandleCompat;
import rocks.tbog.tblauncher.utils.Utilities;
public class IconSelectDialog extends DialogFragment {
private Drawable mSelectedDrawable = null;
private ViewPager mViewPager;
private CustomShapePage mCustomShapePage = null;
private TextView mPreviewLabel;
ActivityResultLauncher imagePickerResult;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
imagePickerResult =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
Context context = IconSelectDialog.this.requireContext();
int resultCode = result.getResultCode();
Intent data = result.getData();
Uri imageUri = data != null ? data.getData() : null;
if (resultCode == Activity.RESULT_OK && imageUri != null) {
Drawable imageDrawable;
try (InputStream is = context.getContentResolver().openInputStream(imageUri)) {
imageDrawable = Drawable.createFromStream(is, imageUri.toString());
} catch (Throwable ignore) {
imageDrawable = null;
}
String filename = getFileName(context, imageUri);
if (imageDrawable != null)
IconSelectDialog.this.addPickedIcon(imageDrawable, filename);
} else if (resultCode == ImagePicker.RESULT_ERROR) {
Toast.makeText(context, ImagePicker.getError(data), Toast.LENGTH_SHORT).show();
}
});
}
public static String getFileName(@NonNull Context context, @NonNull Uri uri) {
String result = null;
if (uri.getScheme().equals("content")) {
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int columnIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (columnIdx != -1)
result = cursor.getString(columnIdx);
}
}
}
if (result == null) {
result = uri.getPath();
int cut = result.lastIndexOf('/');
if (cut != -1) {
result = result.substring(cut + 1);
}
}
return result;
}
private void addPickedIcon(@NonNull Drawable pickedImage, String filename) {
if (!(mViewPager.getAdapter() instanceof PageAdapter))
return;
PageAdapter pageAdapter = (PageAdapter) mViewPager.getAdapter();
for (PageAdapter.Page page : pageAdapter.getPageIterable()) {
page.addPickedIcon(pickedImage, filename);
}
}
@Override
protected int layoutRes() {
return R.layout.dialog_icon_select;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
// make sure we use the dialog context
inflater = inflater.cloneInContext(requireDialog().getContext());
View view = super.onCreateView(inflater, container, savedInstanceState);
if (view == null)
return null;
mPreviewLabel = view.findViewById(R.id.previewLabel);
mViewPager = view.findViewById(R.id.viewPager);
CustomizeUI.setResultListPref(mPreviewLabel);
ResultViewHelper.applyResultItemShadow(mPreviewLabel);
PageAdapter pageAdapter = new PageAdapter();
mViewPager.setAdapter(pageAdapter);
// add system icons
addSystemIcons(inflater);
// add icon packs
ArrayList> iconPacks = addIconPacks(inflater);
pageAdapter.notifyDataSetChanged();
pageAdapter.setupPageView(this);
if (mCustomShapePage instanceof SystemPage) {
((SystemPage) mCustomShapePage).loadIconPackIcons(iconPacks);
}
return view;
}
/**
* Add ViewPager pages for every icon pack
*
* @param inflater used for inflating the page view
* @return a list of pairs with the icon pack package name and icon pack name
*/
@NonNull
private ArrayList> addIconPacks(LayoutInflater inflater) {
Context context = inflater.getContext();
IconsHandler iconsHandler = TBApplication.iconsHandler(context);
Map iconPackNames = iconsHandler.getIconPackNames();
ArrayList> iconPacks = new ArrayList<>(iconPackNames.size());
for (Map.Entry packInfo : iconPackNames.entrySet())
iconPacks.add(new Pair<>(packInfo.getKey(), packInfo.getValue()));
IconPack> iconPack = iconsHandler.getCustomIconPack();
String selectedPackPackageName = iconPack != null ? iconPack.getPackPackageName() : "";
Collections.sort(iconPacks, (o1, o2) -> {
if (selectedPackPackageName.equals(o1.first))
return -1;
if (selectedPackPackageName.equals(o2.first))
return 1;
return o1.second.compareTo(o2.second);
});
for (Pair packInfo : iconPacks) {
String packPackageName = packInfo.first;
String packName = packInfo.second;
if (selectedPackPackageName.equals(packPackageName))
packName = context.getString(R.string.selected_pack, packName);
// add page to ViewPager
addIconPackPage(inflater, mViewPager, packName, packPackageName);
}
return iconPacks;
}
/**
* Add ViewPager page for system icons
*
* @param inflater used for inflating the page view
*/
private void addSystemIcons(LayoutInflater inflater) {
Context context = inflater.getContext();
Bundle args = getArguments() != null ? getArguments() : new Bundle();
if (args.containsKey("componentName")) {
String name = args.getString("componentName", "");
String entryName = args.getString("entryName", "");
String pageName = context.getString(R.string.tab_app_icons, entryName);
ComponentName cn = UserHandleCompat.unflattenComponentName(name);
UserHandleCompat userHandle = UserHandleCompat.fromComponentName(context, name);
mCustomShapePage = addSystemPage(inflater, cn, userHandle, pageName);
} else if (args.containsKey("entryId")) {
String entryId = args.getString("entryId", "");
EntryItem entryItem = TBApplication.dataHandler(context).getPojo(entryId);
if (!(entryItem instanceof StaticEntry)) {
dismiss();
Toast.makeText(Utilities.getActivity(context), context.getString(R.string.entry_not_found, entryId), Toast.LENGTH_LONG).show();
} else {
StaticEntry staticEntry = (StaticEntry) entryItem;
String pageName = context.getString(R.string.tab_static_icons);
mCustomShapePage = addStaticEntryPage(inflater, staticEntry, pageName);
}
} else if (args.containsKey("shortcutId")) {
String packageName = args.getString("packageName", "");
String shortcutData = args.getString("shortcutData", "");
ShortcutRecord shortcutRecord = null;
List shortcutRecordList = DBHelper.getShortcutsNoIcons(context, packageName);
for (ShortcutRecord rec : shortcutRecordList)
if (shortcutData.equals(rec.infoData)) {
shortcutRecord = rec;
break;
}
if (shortcutRecord == null) {
dismiss();
String shortcutId = args.getString("shortcutId", "");
Toast.makeText(Utilities.getActivity(context), context.getString(R.string.entry_not_found, shortcutId), Toast.LENGTH_LONG).show();
} else {
mCustomShapePage = addShortcutPage(inflater, shortcutRecord, shortcutRecord.displayName);
}
} else if (args.containsKey("searchEntryId")) {
String entryName = args.getString("searchName", "");
String pageName = context.getString(R.string.tab_search_icon);
mCustomShapePage = addSearchEntryPage(inflater, entryName, pageName);
} else if (args.containsKey("buttonId")) {
int defaultIcon = args.getInt("defaultIcon");
String pageName = context.getString(R.string.tab_button_icon);
mCustomShapePage = addButtonPage(inflater, defaultIcon, pageName);
}
}
@Override
public void onStart() {
super.onStart();
Dialog dialog = getDialog();
if (dialog instanceof DialogWrapper) {
((DialogWrapper) dialog).setOnWindowFocusChanged((dlg, hasFocus) -> {
if (hasFocus) {
dlg.setOnWindowFocusChanged(null);
//hack: fix the height of the dialog so it doesn't flicker
setFixedHeight(getView());
}
});
}
}
private void setFixedHeight(View view) {
ViewGroup.LayoutParams params = view.getLayoutParams();
params.height = view.getMeasuredHeight();
view.setLayoutParams(params);
}
public void setSelectedDrawable(Drawable selected, Drawable preview) {
Context context = mViewPager.getContext();
mSelectedDrawable = selected;
@StringRes
int label = mSelectedDrawable == null ? R.string.default_icon_preview_label : R.string.custom_icon_preview_label;
mPreviewLabel.setText(label);
int size = UISizes.getResultIconSize(context);
Drawable icon = preview.getConstantState().newDrawable(context.getResources());
icon.setBounds(0, 0, size, size);
mPreviewLabel.setCompoundDrawables(null, null, icon, null);
}
private void addIconPackPage(@NonNull LayoutInflater inflater, ViewGroup container, String packName, String packPackageName) {
View view = inflater.inflate(R.layout.dialog_icon_select_page, container, false);
IconPackPage page = new IconPackPage(packName, packPackageName, view);
PageAdapter adapter = (PageAdapter) mViewPager.getAdapter();
if (adapter != null)
adapter.addPage(page);
}
private CustomShapePage addCustomShapePage(CustomShapePage page) {
PageAdapter adapter = (PageAdapter) mViewPager.getAdapter();
if (adapter != null)
adapter.addPage(page);
return page;
}
private CustomShapePage addSystemPage(LayoutInflater inflater, ComponentName cn, UserHandleCompat userHandle, String pageName) {
View view = inflater.inflate(R.layout.dialog_custom_shape_icon_select_page, mViewPager, false);
CustomShapePage page = new SystemPage(pageName, view, cn, userHandle);
return addCustomShapePage(page);
}
private CustomShapePage addStaticEntryPage(LayoutInflater inflater, StaticEntry staticEntry, String pageName) {
View view = inflater.inflate(R.layout.dialog_custom_shape_icon_select_page, mViewPager, false);
CustomShapePage page = new StaticEntryPage(pageName, view, staticEntry);
return addCustomShapePage(page);
}
private CustomShapePage addSearchEntryPage(LayoutInflater inflater, String entryName, String pageName) {
View view = inflater.inflate(R.layout.dialog_custom_shape_icon_select_page, mViewPager, false);
CustomShapePage page = new DefaultButtonPage(pageName, view, entryName, R.drawable.ic_search, R.string.default_static_icon);
return addCustomShapePage(page);
}
private CustomShapePage addShortcutPage(LayoutInflater inflater, ShortcutRecord shortcutRecord, String pageName) {
View view = inflater.inflate(R.layout.dialog_custom_shape_icon_select_page, mViewPager, false);
CustomShapePage page = new ShortcutPage(pageName, view, shortcutRecord);
return addCustomShapePage(page);
}
private CustomShapePage addButtonPage(LayoutInflater inflater, int defaultIcon, String pageName) {
View view = inflater.inflate(R.layout.dialog_custom_shape_icon_select_page, mViewPager, false);
CustomShapePage page = new DefaultButtonPage(pageName, view, "", defaultIcon, R.string.default_icon);
return addCustomShapePage(page);
}
public ListPopup getIconPackMenu(IconData iconData) {
final Context ctx = requireContext();
LinearAdapter adapter = new LinearAdapter();
adapter.add(new LinearAdapter.ItemTitle(iconData.drawableInfo.getDrawableName()));
adapter.add(new LinearAdapter.Item(ctx, R.string.choose_icon_menu_add));
adapter.add(new LinearAdapter.Item(ctx, R.string.choose_icon_menu_add2));
return ListPopup.create(ctx, adapter)
.setModal(true)
.setOnItemClickListener((a, v, pos) -> {
LinearAdapter.MenuItem item = ((LinearAdapter) a).getItem(pos);
@StringRes int stringId = 0;
if (item instanceof LinearAdapter.Item) {
stringId = ((LinearAdapter.Item) a.getItem(pos)).stringId;
}
if (stringId == R.string.choose_icon_menu_add2) {
if (mCustomShapePage != null)
mCustomShapePage.addIcon(iconData.drawableInfo.getDrawableName(), iconData.getIcon());
// set the first page as current
mViewPager.setCurrentItem(0);
} else if (stringId == R.string.choose_icon_menu_add) {
if (mCustomShapePage != null)
mCustomShapePage.addIcon(iconData.drawableInfo.getDrawableName(), iconData.getIcon());
}
});
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Bundle args = getArguments() != null ? getArguments() : new Bundle();
if (args.containsKey("componentName"))
customIconApp(args);
else if (args.containsKey("entryId"))
customIconStaticEntry(args);
else if (args.containsKey("shortcutId"))
customIconShortcut(args);
else if (args.containsKey("searchEntryId"))
customIconSearchEntry(args);
else if (args.containsKey("buttonId"))
customIconButton(args);
else {
dismiss();
Context ctx = requireContext();
Toast.makeText(Utilities.getActivity(ctx), ctx.getString(R.string.entry_not_found, ""), Toast.LENGTH_LONG).show();
return;
}
// OK button
{
View button = view.findViewById(android.R.id.button1);
button.setOnClickListener(v -> {
onConfirm(mSelectedDrawable);
dismiss();
});
}
// CANCEL button
{
View button = view.findViewById(android.R.id.button2);
button.setOnClickListener(v -> dismiss());
}
}
private void customIconApp(Bundle args) {
Context context = requireContext();
String name = args.getString("componentName", "");
long customIcon = args.getLong("customIcon", 0);
if (name.isEmpty()) {
dismiss();
String entryName = args.getString("entryName", "");
Toast.makeText(Utilities.getActivity(context), context.getString(R.string.entry_not_found, entryName), Toast.LENGTH_LONG).show();
return;
}
IconsHandler iconsHandler = TBApplication.getApplication(context).iconsHandler();
ComponentName cn = UserHandleCompat.unflattenComponentName(name);
UserHandleCompat userHandle = UserHandleCompat.fromComponentName(context, name);
// Preview
initPreviewIcon(mPreviewLabel, ctx -> {
Drawable drawable = customIcon != 0 ? iconsHandler.getCustomIcon(name) : null;
if (drawable == null)
drawable = iconsHandler.getDrawableIconForPackage(cn, userHandle);
return drawable;
});
}
private static void initPreviewIcon(TextView preview, Utilities.GetDrawable asyncGet) {
Utilities.setViewAsync(preview, asyncGet, (view, drawable) -> {
Context ctx = view.getContext();
int size = UISizes.getResultIconSize(ctx);
Drawable icon = drawable.mutate();
icon.setBounds(0, 0, size, size);
((TextView) view).setCompoundDrawables(null, null, icon, null);
int radius = UISizes.getResultListRadius(ctx);
int paddingTop = view.getPaddingTop();
int paddingBottom = view.getPaddingBottom();
view.setPadding(radius / 2, paddingTop, radius / 2, paddingBottom);
});
}
private void customIconStaticEntry(Bundle args) {
Context context = requireContext();
String entryId = args.getString("entryId", "");
EntryItem entryItem = TBApplication.dataHandler(context).getPojo(entryId);
if (!(entryItem instanceof StaticEntry)) {
dismiss();
Toast.makeText(Utilities.getActivity(context), context.getString(R.string.entry_not_found, entryId), Toast.LENGTH_LONG).show();
return;
}
StaticEntry staticEntry = (StaticEntry) entryItem;
// Preview
initPreviewIcon(mPreviewLabel, staticEntry::getIconDrawable);
}
private void customIconSearchEntry(Bundle args) {
Context context = requireContext();
String entryId = args.getString("searchEntryId", "");
EntryItem entryItem = TBApplication.dataHandler(context).getPojo(entryId);
if (!(entryItem instanceof SearchEntry)) {
dismiss();
Toast.makeText(Utilities.getActivity(context), context.getString(R.string.entry_not_found, entryId), Toast.LENGTH_LONG).show();
return;
}
SearchEntry searchEntry = (SearchEntry) entryItem;
// Preview
initPreviewIcon(mPreviewLabel, searchEntry::getIconDrawable);
}
private void customIconShortcut(Bundle args) {
Context context = requireContext();
String shortcutId = args.getString("shortcutId", "");
EntryItem entryItem = TBApplication.dataHandler(context).getPojo(shortcutId);
if (!(entryItem instanceof ShortcutEntry)) {
dismiss();
Toast.makeText(Utilities.getActivity(context), context.getString(R.string.entry_not_found, shortcutId), Toast.LENGTH_LONG).show();
return;
}
ShortcutEntry shortcutEntry = (ShortcutEntry) entryItem;
// Preview
initPreviewIcon(mPreviewLabel, shortcutEntry::getIcon);
}
private void customIconButton(Bundle args) {
final int defaultIcon = args.getInt("defaultIcon", 0);
final String buttonId = args.getString("buttonId", null);
initPreviewIcon(mPreviewLabel, ctx -> {
if (buttonId != null) {
Drawable buttonIcon = TBApplication.iconsHandler(ctx).getButtonIcon(buttonId);
if (buttonIcon != null)
return buttonIcon;
}
return ResourcesCompat.getDrawable(getResources(), defaultIcon, null);
});
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
PageAdapter adapter = (PageAdapter) mViewPager.getAdapter();
if (adapter != null) {
int selectedPage = mViewPager.getCurrentItem();
// allow the adapter to load as needed
mViewPager.addOnPageChangeListener(adapter);
// make sure we load the selected page
adapter.onPageSelected(selectedPage);
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/customicon/IconViewHolder.java
================================================
package rocks.tbog.tblauncher.customicon;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import java.lang.ref.WeakReference;
import rocks.tbog.tblauncher.WorkAsync.AsyncTask;
import rocks.tbog.tblauncher.WorkAsync.TaskRunner;
import rocks.tbog.tblauncher.result.ResultViewHelper;
import rocks.tbog.tblauncher.utils.ViewHolderAdapter;
public class IconViewHolder extends ViewHolderAdapter.ViewHolder {
private final ImageView icon;
private AsyncLoad loader = null;
public IconViewHolder(View view) {
super(view);
icon = view.findViewById(android.R.id.icon);
}
@Override
protected void setContent(IconData content, int position, @NonNull ViewHolderAdapter> adapter) {
if (loader != null)
loader.cancel(false);
loader = new AsyncLoad(this);
loader.execute(content);
}
static class AsyncLoad extends AsyncTask {
private static final String TAG = AsyncLoad.class.getSimpleName();
private final WeakReference holder;
protected AsyncLoad(IconViewHolder holder) {
super();
this.holder = new WeakReference<>(holder);
}
@Override
protected void onPreExecute() {
IconViewHolder h = holder.get();
if (h == null || h.loader != this)
return;
h.icon.setImageDrawable(null);
}
@Override
protected Drawable doInBackground(IconData iconData) {
Drawable drawable = iconData.getIcon();
if (drawable == null)
Log.w(TAG, "drawable `" + iconData.drawableInfo.getDrawableName() + "` from icon pack `" + iconData.iconPack.getPackPackageName() + "` doesn't load");
return drawable;
}
@Override
protected void onPostExecute(Drawable drawable) {
if (drawable == null)
return;
IconViewHolder h = holder.get();
if (h == null || h.loader != this)
return;
h.loader = null;
h.icon.setImageDrawable(drawable);
h.icon.setScaleX(0f);
h.icon.setScaleY(0f);
h.icon.setRotation((drawable.hashCode() & 1) == 1 ? 180f : -180f);
h.icon.animate().scaleX(1f).scaleY(1f).rotation(0f).start();
}
public void execute(IconData content) {
TaskRunner.executeOnExecutor(ResultViewHelper.EXECUTOR_LOAD_ICON, this, content);
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/customicon/PageAdapter.java
================================================
package rocks.tbog.tblauncher.customicon;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Adapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.viewpager.widget.ViewPager;
import java.util.ArrayList;
class PageAdapter extends androidx.viewpager.widget.PagerAdapter implements ViewPager.OnPageChangeListener {
private final ArrayList pageList = new ArrayList<>(0);
private int mScrollState = ViewPager.SCROLL_STATE_IDLE;
void addPage(Page page) {
pageList.add(page);
}
@NonNull
Iterable getPageIterable() {
return pageList;
}
public void setupPageView(@NonNull IconSelectDialog iconSelectDialog) {
// touch listener
Page.OnItemClickListener iconClickListener = (adapter, v, position) -> {
if (adapter instanceof IconAdapter) {
IconData item = ((IconAdapter) adapter).getItem(position);
Drawable icon = item.getIcon();
iconSelectDialog.setSelectedDrawable(icon, icon);
} else if (adapter instanceof CustomShapePage.ShapedIconAdapter) {
CustomShapePage.ShapedIconInfo item = ((CustomShapePage.ShapedIconAdapter) adapter).getItem(position);
if (item instanceof SystemPage.PickedIconInfo) {
if (((SystemPage.PickedIconInfo) item).launchPicker(iconSelectDialog, v))
return;
}
iconSelectDialog.setSelectedDrawable(item.getIcon(), item.getPreview());
}
};
// long touch listener
Page.OnItemClickListener iconLongClickListener = (adapter, v, position) -> {
if (adapter instanceof IconAdapter) {
IconData item = ((IconAdapter) adapter).getItem(position);
iconSelectDialog.getIconPackMenu(item).show(v);
}
};
// setup pages
for (Page page : getPageIterable())
page.setupView(iconSelectDialog, iconClickListener, iconLongClickListener);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
//Log.d("ISDialog", String.format("onPageScrolled %d %.2f", position, positionOffset));
if (mScrollState != ViewPager.SCROLL_STATE_SETTLING) {
Page pageLeft = pageList.get(position);
if (!pageLeft.bDataLoaded)
pageLeft.loadData();
if ((position + 1) < pageList.size()) {
Page pageRight = pageList.get(position + 1);
if (!pageRight.bDataLoaded)
pageRight.loadData();
}
}
}
@Override
public void onPageSelected(int position) {
//Log.d("ISDialog", String.format("onPageSelected %d", position));
Page page = pageList.get(position);
if (!page.bDataLoaded)
page.loadData();
}
@Override
public void onPageScrollStateChanged(int state) {
//Log.d("ISDialog", String.format("onPageScrollStateChanged %d", state));
mScrollState = state;
}
static abstract class Page {
final CharSequence pageName;
final View pageView;
boolean bDataLoaded = false;
public interface OnItemClickListener {
void onItemClick(Adapter adapter, View view, int position);
}
Page(CharSequence name, View view) {
pageName = name;
pageView = view;
}
abstract void setupView(@NonNull DialogFragment dialogFragment, @Nullable OnItemClickListener iconClickListener, @Nullable OnItemClickListener iconLongClickListener);
public void addPickedIcon(@NonNull Drawable pickedImage, String filename) {
// do nothing in the base class, override to handle image picked from gallery
}
void loadData() {
bDataLoaded = true;
}
}
@Override
public int getCount() {
return pageList.size();
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
if (!(object instanceof Page))
throw new IllegalStateException("WTF?");
return ((Page) object).pageView == view;
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return pageList.get(position).pageName;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
Page page = pageList.get(position);
container.addView(page.pageView);
return page;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
if (!(object instanceof Page))
throw new IllegalStateException("WTF?");
Page page = (Page) object;
container.removeView(page.pageView);
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/customicon/ShortcutPage.java
================================================
package rocks.tbog.tblauncher.customicon;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import rocks.tbog.tblauncher.R;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.db.ShortcutRecord;
import rocks.tbog.tblauncher.handler.IconsHandler;
import rocks.tbog.tblauncher.shortcut.ShortcutUtil;
import rocks.tbog.tblauncher.drawable.DrawableUtils;
public class ShortcutPage extends CustomShapePage {
private final ShortcutRecord mShortcutRecord;
ShortcutPage(CharSequence name, View view, ShortcutRecord shortcutRecord) {
super(name, view);
mShortcutRecord = shortcutRecord;
}
@Override
void setupView(@NonNull DialogFragment dialogFragment, @Nullable OnItemClickListener iconClickListener, @Nullable OnItemClickListener iconLongClickListener) {
Context context = dialogFragment.requireContext();
super.setupView(dialogFragment, iconClickListener, iconLongClickListener);
final Drawable defaultIcon;
// default icon
{
Bitmap bitmap = ShortcutUtil.getInitialIcon(context, mShortcutRecord.dbId);
defaultIcon = new BitmapDrawable(dialogFragment.getResources(), bitmap);
Drawable drawable = TBApplication.iconsHandler(context).applyShortcutMask(context, bitmap);
IconsHandler.IconInfo iconHandlerIconInfo = new IconsHandler.IconInfo().setNonAdaptiveIcon(drawable);
ShapedIconInfo iconInfo = new StaticEntryPage.DefaultIconInfo(dialogFragment.getString(R.string.default_static_icon, mShortcutRecord.displayName), iconHandlerIconInfo);
iconInfo.textId = R.string.default_icon;
mShapedIconAdapter.addItem(iconInfo);
}
// add background
{
Drawable shapedDrawable = DrawableUtils.applyIconMaskShape(context, defaultIcon, mShape, mScale, mBackground);
ShapedIconInfo iconInfo = new NamedIconInfo("", shapedDrawable, defaultIcon);
mShapedIconAdapter.addItem(iconInfo);
}
// this will call generateTextIcons
mLettersView.setText(mShortcutRecord.displayName);
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/customicon/StaticEntryPage.java
================================================
package rocks.tbog.tblauncher.customicon;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import rocks.tbog.tblauncher.R;
import rocks.tbog.tblauncher.entry.StaticEntry;
import rocks.tbog.tblauncher.drawable.DrawableUtils;
import rocks.tbog.tblauncher.handler.IconsHandler;
public class StaticEntryPage extends CustomShapePage {
private final StaticEntry mStaticEntry;
StaticEntryPage(CharSequence name, View view, StaticEntry staticEntry) {
super(name, view);
mStaticEntry = staticEntry;
mScale = DrawableUtils.getScaleToFit(mShape);
}
@Override
void setupView(@NonNull DialogFragment dialogFragment, @Nullable OnItemClickListener iconClickListener, @Nullable OnItemClickListener iconLongClickListener) {
Context context = dialogFragment.requireContext();
super.setupView(dialogFragment, iconClickListener, iconLongClickListener);
final Drawable originalDrawable;
// default icon
{
originalDrawable = mStaticEntry.getDefaultDrawable(context);
IconsHandler.IconInfo iconHandlerIconInfo = new IconsHandler.IconInfo().setNonAdaptiveIcon(originalDrawable);
ShapedIconInfo iconInfo = new DefaultIconInfo(dialogFragment.getString(R.string.default_static_icon, mStaticEntry.getName()), iconHandlerIconInfo);
mShapedIconAdapter.addItem(iconInfo);
}
// customizable default icon
{
Drawable shapedDrawable = DrawableUtils.applyIconMaskShape(context, originalDrawable, mShape, mScale, mBackground);
ShapedIconInfo iconInfo = new NamedIconInfo(mStaticEntry.getName(), shapedDrawable, originalDrawable);
mShapedIconAdapter.addItem(iconInfo);
}
// this will call generateTextIcons
mLettersView.setText(mStaticEntry.getName());
}
static class DefaultIconInfo extends CustomShapePage.DefaultIconInfo {
final String name;
DefaultIconInfo(@NonNull String name, IconsHandler.IconInfo icon) {
super(icon);
this.name = name;
textId = R.string.default_icon;
}
@Nullable
@Override
CharSequence getText() {
return name;
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/customicon/SystemPage.java
================================================
package rocks.tbog.tblauncher.customicon;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.Pair;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.collection.ArraySet;
import androidx.fragment.app.DialogFragment;
import java.util.ArrayList;
import java.util.List;
import rocks.tbog.tblauncher.R;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.drawable.DrawableUtils;
import rocks.tbog.tblauncher.handler.IconsHandler;
import rocks.tbog.tblauncher.icons.DrawableInfo;
import rocks.tbog.tblauncher.icons.IconPackXML;
import rocks.tbog.tblauncher.utils.UserHandleCompat;
import rocks.tbog.tblauncher.utils.Utilities;
public class SystemPage extends CustomShapePage {
private final ComponentName componentName;
private final UserHandleCompat userHandle;
SystemPage(CharSequence name, View view, ComponentName cn, UserHandleCompat uh) {
super(name, view);
componentName = cn;
userHandle = uh;
}
@Override
void setupView(@NonNull DialogFragment dialogFragment, @Nullable OnItemClickListener iconClickListener, @Nullable OnItemClickListener iconLongClickListener) {
super.setupView(dialogFragment, iconClickListener, iconLongClickListener);
addSystemIcons(dialogFragment.getContext(), mShapedIconAdapter);
// this will call generateTextIcons
//mLettersView.setText(pageName);
}
private void addSystemIcons(Context context, ShapedIconAdapter adapter) {
ArraySet dSet = new ArraySet<>(3);
// add default icon
{
IconsHandler iconsHandler = TBApplication.getApplication(context).iconsHandler();
IconsHandler.IconInfo icon = iconsHandler.getIconForPackage(componentName, userHandle);
//checkDuplicateDrawable(dSet, drawable);
ShapedIconInfo iconInfo = new DefaultIconInfo(icon);
iconInfo.textId = R.string.default_icon;
adapter.addItem(iconInfo);
}
// add getActivityIcon(componentName)
{
Drawable drawable = null;
try {
drawable = context.getPackageManager().getActivityIcon(componentName);
} catch (PackageManager.NameNotFoundException ignored) {
}
if (drawable != null) {
if (checkDuplicateDrawable(dSet, drawable)) {
{
Drawable shapedDrawable = DrawableUtils.applyIconMaskShape(context, drawable, mShape, mScale, mBackground);
addQuickOption(R.string.custom_icon_activity, shapedDrawable, drawable, adapter);
}
if (DrawableUtils.isAdaptiveIconDrawable(drawable)) {
Drawable noBackground = DrawableUtils.applyAdaptiveIconBackgroundShape(context, drawable, DrawableUtils.SHAPE_SQUARE, true);
Drawable shapedDrawable = DrawableUtils.applyIconMaskShape(context, noBackground, mShape, mScale, mBackground);
addQuickOption(R.string.custom_icon_activity_adaptive_no_background, shapedDrawable, noBackground, adapter);
}
}
}
}
// add getApplicationIcon(packageName)
{
Drawable drawable = null;
try {
drawable = context.getPackageManager().getApplicationIcon(componentName.getPackageName());
} catch (PackageManager.NameNotFoundException ignored) {
}
if (drawable != null) {
if (checkDuplicateDrawable(dSet, drawable)) {
Drawable shapedDrawable = DrawableUtils.applyIconMaskShape(context, drawable, mShape, mScale, mBackground);
addQuickOption(R.string.custom_icon_application, shapedDrawable, drawable, adapter);
}
}
}
// add Activity BadgedIcon
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
LauncherApps launcher = (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
assert launcher != null;
List icons = launcher.getActivityList(componentName.getPackageName(), userHandle.getRealHandle());
for (LauncherActivityInfo info : icons) {
Drawable drawable = info.getBadgedIcon(0);
if (drawable != null) {
if (checkDuplicateDrawable(dSet, drawable)) {
Drawable shapedDrawable = DrawableUtils.applyIconMaskShape(context, drawable, mShape, mScale, mBackground);
addQuickOption(R.string.custom_icon_badged, shapedDrawable, drawable, adapter);
}
}
}
}
}
private boolean checkDuplicateDrawable(ArraySet set, Drawable drawable) {
Bitmap b = null;
if (drawable instanceof BitmapDrawable)
b = ((BitmapDrawable) drawable).getBitmap();
if (set.contains(b))
return false;
set.add(b);
return true;
}
private static void addQuickOption(@StringRes int textId, Drawable shapedDrawable, Drawable drawable, ShapedIconAdapter adapter) {
if (!(shapedDrawable instanceof BitmapDrawable))
return;
ShapedIconInfo iconInfo = new ShapedIconInfo(shapedDrawable, drawable);
iconInfo.textId = textId;
adapter.addItem(iconInfo);
}
public void loadIconPackIcons(List> iconPacks) {
if (iconPacks.isEmpty())
return;
final Context ctx = pageView.getContext();
final ShapedIconInfo placeholderItem = new ShapedIconInfo(DrawableUtils.getProgressBarIndeterminate(ctx), null);
placeholderItem.textId = R.string.icon_pack_loading;
{
mShapedIconAdapter.addItem(placeholderItem);
}
final ArrayList options = new ArrayList<>();
Utilities.runAsync((t) -> {
for (Pair packInfo : iconPacks) {
String packPackageName = packInfo.first;
String packName = packInfo.second;
Activity activity = Utilities.getActivity(pageView);
if (activity != null) {
IconPackXML pack = TBApplication.iconPackCache(activity).getIconPack(packPackageName);
pack.load(activity.getPackageManager());
DrawableInfo info = pack.getComponentDrawable(activity, componentName, userHandle);
Drawable drawable = pack.getDrawable(info);
if (drawable != null) {
Drawable shapedDrawable = DrawableUtils.applyIconMaskShape(activity, drawable, mShape, mScale, mBackground);
NamedIconInfo iconInfo = new NamedIconInfo(packName, shapedDrawable, drawable);
options.add(iconInfo);
}
} else {
break;
}
}
}, (t) -> {
Activity activity = Utilities.getActivity(pageView);
if (activity != null) {
final ShapedIconAdapter adapter = mShapedIconAdapter;
adapter.removeItem(placeholderItem);
adapter.addItems(options);
}
});
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/ActionProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import android.content.Context;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import rocks.tbog.tblauncher.R;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.TBLauncherActivity;
import rocks.tbog.tblauncher.entry.ActionEntry;
import rocks.tbog.tblauncher.entry.EntryItem;
import rocks.tbog.tblauncher.handler.DataHandler;
import rocks.tbog.tblauncher.searcher.HistorySearcher;
import rocks.tbog.tblauncher.searcher.Searcher;
import rocks.tbog.tblauncher.searcher.TagList;
import rocks.tbog.tblauncher.ui.ListPopup;
import rocks.tbog.tblauncher.utils.DebugInfo;
public class ActionProvider extends DBProvider {
private static final ActionEntry[] s_entries = new ActionEntry[19];
@StringRes
private static final int[] s_names = new int[19];
static {
int cnt = 0;
{
String id = ActionEntry.SCHEME + "toggle/grid";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_grid);
actionEntry.setAction((v, flags) -> {
TBLauncherActivity act = TBApplication.launcherActivity(v.getContext());
if (act == null)
return;
// toggle grid/list layout
if (act.behaviour.isGridLayout())
act.behaviour.setListLayout();
else
act.behaviour.setGridLayout();
});
s_names[cnt] = R.string.action_toggle_grid;
s_entries[cnt++] = actionEntry;
}
// show apps sorted by name
{
String id = ActionEntry.SCHEME + "show/apps/byName";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_apps_list_az);
actionEntry.setAction((v, flags) -> {
TBApplication app = TBApplication.getApplication(v.getContext());
TBLauncherActivity act = app.launcherActivity();
if (act == null)
return;
Provider extends EntryItem> provider = app.getDataHandler().getAppProvider();
act.quickList.toggleProvider(v, provider, EntryItem.NAME_COMPARATOR);
act.behaviour.setListLayout();
});
s_names[cnt] = R.string.action_show_apps;
s_entries[cnt++] = actionEntry;
}
// show apps sorted by name in reverse order
{
String id = ActionEntry.SCHEME + "show/apps/byNameReversed";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_apps_list_za);
actionEntry.setAction((v, flags) -> {
TBApplication app = TBApplication.getApplication(v.getContext());
TBLauncherActivity act = app.launcherActivity();
if (act == null)
return;
Provider extends EntryItem> provider = app.getDataHandler().getAppProvider();
act.quickList.toggleProvider(v, provider, Collections.reverseOrder(EntryItem.NAME_COMPARATOR));
act.behaviour.setListLayout();
});
s_names[cnt] = R.string.action_show_apps_reversed;
s_entries[cnt++] = actionEntry;
}
// show apps in a 4 column grid sorted by name
{
String id = ActionEntry.SCHEME + "show/grid4c/apps/byName";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_apps_grid_az);
actionEntry.setAction((v, flags) -> {
TBApplication app = TBApplication.getApplication(v.getContext());
TBLauncherActivity act = app.launcherActivity();
if (act == null)
return;
Provider extends EntryItem> provider = app.getDataHandler().getAppProvider();
act.quickList.toggleProvider(v, provider, EntryItem.NAME_COMPARATOR);
act.behaviour.setGridLayout(4);
});
s_names[cnt] = R.string.action_show_apps_grid4;
s_entries[cnt++] = actionEntry;
}
// show apps in a 4 column grid sorted by name in reverse order
{
String id = ActionEntry.SCHEME + "show/grid4c/apps/byNameReversed";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_apps_grid_za);
actionEntry.setAction((v, flags) -> {
TBApplication app = TBApplication.getApplication(v.getContext());
TBLauncherActivity act = app.launcherActivity();
if (act == null)
return;
Provider extends EntryItem> provider = app.getDataHandler().getAppProvider();
act.quickList.toggleProvider(v, provider, Collections.reverseOrder(EntryItem.NAME_COMPARATOR));
act.behaviour.setGridLayout(4);
});
s_names[cnt] = R.string.action_show_apps_grid4_reversed;
s_entries[cnt++] = actionEntry;
}
// show contacts sorted by name
{
String id = ActionEntry.SCHEME + "show/contacts/byName";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_contacts_az);
actionEntry.setAction((v, flags) -> {
TBApplication app = TBApplication.getApplication(v.getContext());
TBLauncherActivity act = app.launcherActivity();
if (act == null)
return;
Provider extends EntryItem> provider = app.getDataHandler().getContactsProvider();
act.quickList.toggleProvider(v, provider, EntryItem.NAME_COMPARATOR);
});
s_names[cnt] = R.string.action_show_contacts;
s_entries[cnt++] = actionEntry;
}
// show contacts sorted by name in reverse order
{
String id = ActionEntry.SCHEME + "show/contacts/byNameReversed";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_contacts_za);
actionEntry.setAction((v, flags) -> {
TBApplication app = TBApplication.getApplication(v.getContext());
TBLauncherActivity act = app.launcherActivity();
if (act == null)
return;
Provider extends EntryItem> provider = app.getDataHandler().getContactsProvider();
act.quickList.toggleProvider(v, provider, Collections.reverseOrder(EntryItem.NAME_COMPARATOR));
});
s_names[cnt] = R.string.action_show_contacts_reversed;
s_entries[cnt++] = actionEntry;
}
// show shortcuts sorted by name
{
String id = ActionEntry.SCHEME + "show/shortcuts/byName";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_shortcuts_az);
actionEntry.setAction((v, flags) -> {
TBApplication app = TBApplication.getApplication(v.getContext());
TBLauncherActivity act = app.launcherActivity();
if (act == null)
return;
Provider extends EntryItem> provider = app.getDataHandler().getShortcutsProvider();
act.quickList.toggleProvider(v, provider, EntryItem.NAME_COMPARATOR);
});
s_names[cnt] = R.string.action_show_shortcuts;
s_entries[cnt++] = actionEntry;
}
// show shortcuts sorted by name in reverse order
{
String id = ActionEntry.SCHEME + "show/shortcuts/byNameReversed";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_shortcuts_za);
actionEntry.setAction((v, flags) -> {
TBApplication app = TBApplication.getApplication(v.getContext());
TBLauncherActivity act = app.launcherActivity();
if (act == null)
return;
Provider extends EntryItem> provider = app.getDataHandler().getShortcutsProvider();
act.quickList.toggleProvider(v, provider, Collections.reverseOrder(EntryItem.NAME_COMPARATOR));
});
s_names[cnt] = R.string.action_show_shortcuts_reversed;
s_entries[cnt++] = actionEntry;
}
// show favorites sorted by name (removed by load task if not enabled)
{
String id = ActionEntry.SCHEME + "show/favorites/byName";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_favorites);
actionEntry.setAction((v, flags) -> {
TBApplication app = TBApplication.getApplication(v.getContext());
TBLauncherActivity act = app.launcherActivity();
if (act == null)
return;
ModProvider provider = app.getDataHandler().getModProvider();
act.quickList.toggleProvider(v, provider, EntryItem.NAME_COMPARATOR);
});
s_names[cnt] = R.string.action_show_favorites;
s_entries[cnt++] = actionEntry;
}
// show history sorted by how recent it was accessed
{
String id = ActionEntry.SCHEME + "show/history/recency";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_history);
actionEntry.setAction((v, f) -> toggleSearch(v, "recency", HistorySearcher.class));
s_names[cnt] = R.string.action_show_history_recency;
s_entries[cnt++] = actionEntry;
}
// show history sorted by how frequent it was accessed
{
String id = ActionEntry.SCHEME + "show/history/frequency";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_history);
actionEntry.setAction((v, f) -> toggleSearch(v, "frequency", HistorySearcher.class));
s_names[cnt] = R.string.action_show_history_frequency;
s_entries[cnt++] = actionEntry;
}
// show history sorted based on frequency * recency
// frequency = #launches_for_app / #all_launches
// recency = 1 / position_of_app_in_normal_history
{
String id = ActionEntry.SCHEME + "show/history/frecency";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_history);
actionEntry.setAction((v, f) -> toggleSearch(v, "frecency", HistorySearcher.class));
s_names[cnt] = R.string.action_show_history_frecency;
s_entries[cnt++] = actionEntry;
}
// show history sorted by how frequent it was accessed in the last 36 hours
{
String id = ActionEntry.SCHEME + "show/history/adaptive";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_history);
actionEntry.setAction((v, f) -> toggleSearch(v, "adaptive", HistorySearcher.class));
s_names[cnt] = R.string.action_show_history_adaptive;
s_entries[cnt++] = actionEntry;
}
{
String id = ActionEntry.SCHEME + "show/untagged";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_untagged);
actionEntry.setAction((v, f) -> toggleSearch(v, "untagged", TagList.class));
s_names[cnt] = R.string.action_show_untagged;
s_entries[cnt++] = actionEntry;
}
{
String id = ActionEntry.SCHEME + "show/tags/menu";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_tags);
actionEntry.setAction((v, flags) -> {
Context ctx = v.getContext();
TBApplication app = TBApplication.getApplication(ctx);
ListPopup menu = app.tagsHandler().getTagsMenu(ctx);
app.registerPopup(menu);
menu.show(v);
});
s_names[cnt] = R.string.show_tags_menu;
s_entries[cnt++] = actionEntry;
}
{
String id = ActionEntry.SCHEME + "show/tags/list";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_tags);
actionEntry.setAction((v, f) -> toggleSearch(v, "list", TagList.class));
s_names[cnt] = R.string.show_tags_list;
s_entries[cnt++] = actionEntry;
}
{
String id = ActionEntry.SCHEME + "show/tags/listReversed";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_tags);
actionEntry.setAction((v, f) -> toggleSearch(v, "listReversed", TagList.class));
s_names[cnt] = R.string.show_tags_list_reversed;
s_entries[cnt++] = actionEntry;
}
{
String id = ActionEntry.SCHEME + "reload/providers";
ActionEntry actionEntry = new ActionEntry(id, R.drawable.ic_refresh);
actionEntry.setAction((v, flags) -> {
Context ctx = v.getContext();
TBApplication.dataHandler(ctx).reloadProviders();
});
s_names[cnt] = R.string.action_reload;
s_entries[cnt++] = actionEntry;
}
//noinspection ConstantConditions
if (cnt != s_entries.length || cnt != s_names.length)
throw new IllegalStateException("ActionEntry static list size");
}
private static void toggleSearch(@NonNull View v, @NonNull String query, @NonNull Class extends Searcher> searcherClass) {
TBLauncherActivity act = TBApplication.launcherActivity(v.getContext());
if (act != null)
act.quickList.toggleSearch(v, query, searcherClass);
}
public ActionProvider(@NonNull Context context) {
super(context);
}
@Override
protected DBLoader newLoadTask() {
return new UpdateFromModsLoader(this, s_entries, s_names) {
@Override
public List getEntryItems(DataHandler dataHandler) {
List entries = super.getEntryItems(dataHandler);
Context context = dataHandler.getContext();
if (context == null || !DebugInfo.enableFavorites(context)) {
// remove debug entry
for (Iterator iterator = entries.iterator(); iterator.hasNext(); ) {
ActionEntry entry = iterator.next();
if (entry.id.endsWith("show/favorites/byName"))
iterator.remove();
}
}
return entries;
}
};
}
@Override
public boolean mayFindById(@NonNull String id) {
return id.startsWith(ActionEntry.SCHEME);
}
@NonNull
public String getDefaultName(@NonNull String id) {
for (int idx = 0; idx < s_entries.length; idx += 1) {
if (id.equals(s_entries[idx].id))
return context.getString(s_names[idx]);
}
return "null";
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/AppCacheProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import rocks.tbog.tblauncher.entry.AppEntry;
import rocks.tbog.tblauncher.handler.AppsHandler;
import rocks.tbog.tblauncher.normalizer.StringNormalizer;
import rocks.tbog.tblauncher.searcher.ISearcher;
import rocks.tbog.tblauncher.utils.FuzzyScore;
import rocks.tbog.tblauncher.utils.Timer;
public class AppCacheProvider implements IProvider {
final static String TAG = "AppCP";
final private AppsHandler appsHandler;
public AppCacheProvider(@NonNull AppsHandler handler) {
appsHandler = handler;
}
@WorkerThread
@Override
public void requestResults(String query, ISearcher searcher) {
StringNormalizer.Result queryNormalized = StringNormalizer.normalizeWithResult(query, false);
if (queryNormalized.codePoints.length == 0) {
return;
}
final CountDownLatch latch = new CountDownLatch(1);
// notify that the tags are loaded
appsHandler.runWhenLoaded(latch::countDown);
// wait for the tags to load
try {
latch.await();
} catch (InterruptedException e) {
Log.e(TAG, "waiting for TagsHandler", e);
}
final Collection entries = appsHandler.getAllApps();
FuzzyScore fuzzyScore = new FuzzyScore(queryNormalized.codePoints);
EntryToResultUtils.tagsCheckResults(entries, fuzzyScore, searcher);
}
public void reload(boolean cancelCurrentLoadTask) {
}
@Override
public boolean isLoaded() {
return true;
}
@Override
public Timer getLoadDuration() {
return null;
}
@Override
public void setDirty() {
// do nothing, we already have the full list of items
}
@Override
public int getLoadStep() {
return LOAD_STEP_1;
}
@Override
public boolean mayFindById(@NonNull String id) {
return false;
}
@Override
public AppEntry findById(@NonNull String id) {
return null;
}
@Nullable
@Override
public List getPojos() {
return null;
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/AppProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.LauncherApps;
import android.os.Build;
import android.os.Process;
import android.os.UserManager;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.Objects;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.broadcast.PackageAddedRemovedHandler;
import rocks.tbog.tblauncher.entry.AppEntry;
import rocks.tbog.tblauncher.loader.LoadAppEntry;
import rocks.tbog.tblauncher.loader.LoadCacheApps;
import rocks.tbog.tblauncher.searcher.ISearcher;
import rocks.tbog.tblauncher.utils.UserHandleCompat;
public class AppProvider extends Provider {
boolean mInitialLoad = true;
AppsCallback mAppsCallback = null;
final BroadcastReceiver mProfileReceiver = new BroadcastReceiver() {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onReceive(Context context, Intent intent) {
if (Objects.equals(intent.getAction(), Intent.ACTION_MANAGED_PROFILE_ADDED)) {
AppProvider.this.reload(true);
} else if (Objects.equals(intent.getAction(), Intent.ACTION_MANAGED_PROFILE_REMOVED)) {
// android.os.UserHandle profile = intent.getParcelableExtra(Intent.EXTRA_USER);
// final UserManager manager = (UserManager) AppProvider.this.getSystemService(Context.USER_SERVICE);
// assert manager != null;
// UserHandleCompat user = new UserHandleCompat(manager.getSerialNumberForUser(profile), profile);
// DataHandler dataHandler = TBApplication.getApplication(context).getDataHandler();
// dataHandler.removeFromExcluded(user);
// dataHandler.removeFromMods(user);
AppProvider.this.reload(true);
}
}
};
final PackageAddedRemovedHandler mPackageAddedRemovedHandler = new PackageAddedRemovedHandler();
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
static class AppsCallback extends LauncherApps.Callback {
private final Context context;
AppsCallback(Context context) {
this.context = context;
}
@Override
public void onPackageAdded(String packageName, android.os.UserHandle user) {
handleEvent(Intent.ACTION_PACKAGE_ADDED, packageName, user, false);
}
@Override
public void onPackageChanged(String packageName, android.os.UserHandle user) {
handleEvent(Intent.ACTION_PACKAGE_CHANGED, packageName, user, true);
}
@Override
public void onPackageRemoved(String packageName, android.os.UserHandle user) {
handleEvent(Intent.ACTION_PACKAGE_REMOVED, packageName, user, false);
}
@Override
public void onPackagesAvailable(String[] packageNames, android.os.UserHandle user, boolean replacing) {
handleEvent(Intent.ACTION_MEDIA_MOUNTED, null, user, replacing);
}
@Override
public void onPackagesUnavailable(String[] packageNames, android.os.UserHandle user, boolean replacing) {
handleEvent(Intent.ACTION_MEDIA_UNMOUNTED, null, user, replacing);
}
@Override
public void onPackagesSuspended(String[] packageNames, android.os.UserHandle user) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
handleEvent(Intent.ACTION_PACKAGES_SUSPENDED, null, user, false);
}
}
@Override
public void onPackagesUnsuspended(String[] packageNames, android.os.UserHandle user) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
handleEvent(Intent.ACTION_PACKAGES_UNSUSPENDED, null, user, false);
}
}
private void handleEvent(String action, String packageName, android.os.UserHandle user, boolean replacing) {
if (!Process.myUserHandle().equals(user)) {
final UserManager manager = (UserManager) context.getSystemService(Context.USER_SERVICE);
PackageAddedRemovedHandler.handleEvent(context,
action,
packageName,
new UserHandleCompat(manager.getSerialNumberForUser(user), user),
replacing
);
}
}
}
@Override
public void onCreate() {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Package install/uninstall events for the main
// profile are still handled using PackageAddedRemovedHandler itself
final LauncherApps launcher = (LauncherApps) this.getSystemService(Context.LAUNCHER_APPS_SERVICE);
assert launcher != null;
mAppsCallback = new AppsCallback(this);
launcher.registerCallback(mAppsCallback);
// Try to clean up app-related data when profile is removed
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
ActivityCompat.registerReceiver(this, mProfileReceiver, filter, ContextCompat.RECEIVER_EXPORTED);
}
// Get notified when app changes on standard user profile
IntentFilter appChangedFilter = new IntentFilter();
appChangedFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
appChangedFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
appChangedFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
appChangedFilter.addAction(Intent.ACTION_MEDIA_MOUNTED);
appChangedFilter.addAction(Intent.ACTION_MEDIA_REMOVED);
appChangedFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
appChangedFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
appChangedFilter.addAction(Intent.ACTION_PACKAGES_SUSPENDED);
appChangedFilter.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED);
}
appChangedFilter.addDataScheme("package");
appChangedFilter.addDataScheme("file");
ActivityCompat.registerReceiver(this, mPackageAddedRemovedHandler, appChangedFilter, ContextCompat.RECEIVER_EXPORTED);
super.onCreate();
}
@Override
public void onDestroy() {
unregisterReceiver(mProfileReceiver);
unregisterReceiver(mPackageAddedRemovedHandler);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
LauncherApps launcher = (LauncherApps) this.getSystemService(Context.LAUNCHER_APPS_SERVICE);
assert launcher != null;
launcher.unregisterCallback(mAppsCallback);
}
super.onDestroy();
}
public void reload(boolean cancelCurrentLoadTask) {
super.reload(cancelCurrentLoadTask);
if (!isLoaded() && !isLoading()) {
if (mInitialLoad) {
// Use DB cache to speed things up. We'll reload after.
this.initialize(new LoadCacheApps(this));
} else {
this.initialize(new LoadAppEntry(this));
}
}
}
@Override
public void loadOver(ArrayList results) {
super.loadOver(results);
if (mInitialLoad) {
mInitialLoad = false;
// Got DB cache. Do a reload later.
TBApplication.dataHandler(this).runAfterLoadOver(() -> {
this.reload(false);
});
} else {
TBApplication.appsHandler(this).setAppCache(results);
}
}
/**
* @param query The string to search for
* @param searcher The receiver of results
*/
@WorkerThread
@Override
public void requestResults(String query, ISearcher searcher) {
for (AppEntry pojo : pojos)
pojo.resetResultInfo();
EntryToResultUtils.recursiveWordCheck(pojos, query, searcher, EntryToResultUtils::tagsCheckResults, AppEntry.class);
}
/**
* Return a Pojo
*
* @param id we're looking for
* @return an AppEntry, or null
*/
@Override
public AppEntry findById(@NonNull String id) {
for (AppEntry pojo : pojos) {
if (pojo.id.equals(id)) {
return pojo;
}
}
return null;
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/CalculatorProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.util.ArrayDeque;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import rocks.tbog.tblauncher.calculator.Calculator;
import rocks.tbog.tblauncher.calculator.Result;
import rocks.tbog.tblauncher.calculator.ShuntingYard;
import rocks.tbog.tblauncher.calculator.Tokenizer;
import rocks.tbog.tblauncher.entry.CalculatorEntry;
import rocks.tbog.tblauncher.searcher.ISearcher;
public class CalculatorProvider extends SimpleProvider {
private final Pattern computableRegexp;
// A regexp to detect plain numbers (including phone numbers)
private final Pattern numberOnlyRegexp;
private final NumberFormat LOCALIZED_NUMBER_FORMATTER = NumberFormat.getInstance();
public CalculatorProvider() {
//This should try to match as much as possible without going out of the expression,
//even if the expression is not actually a computable operation.
computableRegexp = Pattern.compile("^[\\-.,\\d+*×x/÷^'()]+$");
numberOnlyRegexp = Pattern.compile("^\\+?[.,()\\d]+$");
}
@Override
public void requestResults(String query, ISearcher searcher) {
String spacelessQuery = query.replaceAll("\\s+", "");
// Now create matcher object.
Matcher m = computableRegexp.matcher(spacelessQuery);
if (m.find()) {
if (numberOnlyRegexp.matcher(spacelessQuery).find()) {
return;
}
String operation = m.group();
Result> tokenized = Tokenizer.tokenize(operation);
String readableResult;
if (tokenized.syntacticalError) {
return;
} else if (tokenized.arithmeticalError) {
return;
} else {
Result> posfixed = ShuntingYard.infixToPostfix(tokenized.result);
if (posfixed.syntacticalError) {
return;
} else if (posfixed.arithmeticalError) {
return;
} else {
Result result = Calculator.calculateExpression(posfixed.result);
if (result.syntacticalError) {
return;
} else if (result.arithmeticalError) {
return;
} else {
String localizedNumber = LOCALIZED_NUMBER_FORMATTER.format(result.result);
readableResult = " = " + localizedNumber;
}
}
}
String queryProcessed = operation + readableResult;
CalculatorEntry pojo = new CalculatorEntry(queryProcessed);
pojo.setRelevance(pojo.normalizedName, null);
pojo.boostRelevance(19);
searcher.addResult(pojo);
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/ContactsProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.provider.ContactsContract;
import android.util.Log;
import androidx.annotation.WorkerThread;
import androidx.preference.PreferenceManager;
import java.util.Collection;
import rocks.tbog.tblauncher.Permission;
import rocks.tbog.tblauncher.entry.ContactEntry;
import rocks.tbog.tblauncher.loader.LoadContactsEntry;
import rocks.tbog.tblauncher.normalizer.PhoneNormalizer;
import rocks.tbog.tblauncher.normalizer.StringNormalizer;
import rocks.tbog.tblauncher.searcher.ISearcher;
import rocks.tbog.tblauncher.utils.FuzzyScore;
public class ContactsProvider extends Provider {
private final static String TAG = "ContactsProvider";
private final ContentObserver cObserver = new ContentObserver(null) {
@Override
public void onChange(boolean selfChange) {
//reload contacts
Log.i(TAG, "Contacts changed, reloading provider.");
reload(true);
}
};
public void reload(boolean cancelCurrentLoadTask) {
super.reload(cancelCurrentLoadTask);
if (!isLoaded() && !isLoading())
this.initialize(new LoadContactsEntry(this));
}
@Override
public void onCreate() {
super.onCreate();
// register content observer if we have permission
if (Permission.checkPermission(this, Permission.PERMISSION_READ_CONTACTS)) {
getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, false, cObserver);
} else {
Permission.askPermission(Permission.PERMISSION_READ_CONTACTS, new Permission.PermissionResultListener() {
@Override
public void onGranted() {
// Great! Reload the contact provider. We're done :)
reload(true);
}
@Override
public void onDenied() {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(ContactsProvider.this);
pref.edit().putBoolean("enable-contacts", false).apply();
}
});
}
}
@Override
public void onDestroy() {
super.onDestroy();
//deregister content observer
getContentResolver().unregisterContentObserver(cObserver);
}
@Override
public void requestResults(String query, ISearcher searcher) {
for (ContactEntry pojo : pojos)
pojo.resetResultInfo();
EntryToResultUtils.recursiveWordCheck(pojos, query, searcher, ContactsProvider::checkResults, ContactEntry.class);
}
@WorkerThread
public static void checkResults(Collection entries, FuzzyScore fuzzyScore, ISearcher searcher) {
Log.d(TAG, "checkResults count=" + entries.size() + " " + fuzzyScore);
for (ContactEntry entry : entries) {
FuzzyScore.MatchInfo scoreInfo = fuzzyScore.match(entry.normalizedName.codePoints);
StringNormalizer.Result matchedText = entry.normalizedName;
FuzzyScore.MatchInfo matchedInfo = FuzzyScore.MatchInfo.copyOrNewInstance(scoreInfo, null);
if (entry.normalizedNickname != null) {
scoreInfo = fuzzyScore.match(entry.normalizedNickname.codePoints);
if (scoreInfo.match && (!matchedInfo.match || scoreInfo.score > matchedInfo.score)) {
matchedText = entry.normalizedNickname;
matchedInfo = FuzzyScore.MatchInfo.copyOrNewInstance(scoreInfo, matchedInfo);
}
}
if (!matchedInfo.match && entry.normalizedPhone != null && fuzzyScore.getPatternLength() > 2) {
// search for the phone number
scoreInfo = fuzzyScore.match(entry.normalizedPhone.codePoints);
if (scoreInfo.match && scoreInfo.score > matchedInfo.score) {
matchedText = entry.normalizedPhone;
matchedInfo = FuzzyScore.MatchInfo.copyOrNewInstance(scoreInfo, matchedInfo);
}
}
entry.addResultMatch(matchedText, matchedInfo);
if (matchedInfo.match) {
int boost = Math.min(30, entry.getTimesContacted());
if (entry.isStarred()) {
boost += 40;
}
entry.boostRelevance(boost);
if (!searcher.addResult(entry))
return;
}
}
}
/**
* Find a ContactsPojo from a phoneNumber
* If many contacts match, the one most often contacted will be returned
*
* @param phoneNumber phone number to find (will be normalized)
* @return a contactpojo, or null.
*/
public ContactEntry findByPhone(String phoneNumber) {
StringNormalizer.Result simplifiedPhoneNumber = PhoneNormalizer.simplifyPhoneNumber(phoneNumber);
for (ContactEntry pojo : pojos) {
if (pojo.normalizedPhone.equals(simplifiedPhoneNumber)) {
return pojo;
}
}
return null;
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/DBProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import android.content.Context;
import android.util.Log;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import rocks.tbog.tblauncher.BuildConfig;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.TBLauncherActivity;
import rocks.tbog.tblauncher.WorkAsync.AsyncTask;
import rocks.tbog.tblauncher.WorkAsync.TaskRunner;
import rocks.tbog.tblauncher.entry.EntryItem;
import rocks.tbog.tblauncher.handler.DataHandler;
import rocks.tbog.tblauncher.searcher.ISearcher;
import rocks.tbog.tblauncher.utils.Timer;
public abstract class DBProvider implements IProvider {
final Context context;
protected List entryList = new ArrayList<>();
private boolean mIsLoaded = false;
private DBLoader mLoadTask = null;
protected final Timer mTimer = new Timer();
public DBProvider(Context context) {
this.context = context;
}
@Override
public void requestResults(String query, ISearcher searcher) {
}
@Override
public void reload(boolean cancelCurrentLoadTask) {
if (!cancelCurrentLoadTask && mLoadTask != null)
return;
setDirty();
Log.i(Provider.TAG, "Starting provider: " + this.getClass().getSimpleName());
mTimer.start();
mLoadTask = newLoadTask();
mLoadTask.execute();
}
protected abstract DBLoader newLoadTask();
@Override
public boolean isLoaded() {
return mIsLoaded;
}
@Override
public Timer getLoadDuration() {
return mTimer;
}
protected void setLoaded() {
mIsLoaded = true;
}
@Override
public void setDirty() {
// mark this as not loaded and wait for DataHandler to call reload
mIsLoaded = false;
if (mLoadTask != null)
mLoadTask.cancel(true);
mLoadTask = null;
}
@Override
public int getLoadStep() {
return LOAD_STEP_2;
}
/**
* Whether or not this provider may be able to find a pojo with the specified id
*
* @param id id we're looking for
* @return true if the provider can handle the query; does not guarantee it will!
*/
@Override
public boolean mayFindById(@NonNull String id) {
return false;
}
/**
* Try to find a record by its id
*
* @param id id we're looking for
* @return null if not found
*/
@Override
public T findById(@NonNull String id) {
for (T entryItem : entryList) {
if (entryItem.id.equals(id)) {
return entryItem;
}
}
return null;
}
@Nullable
@Override
public List getPojos() {
if (BuildConfig.DEBUG)
return Collections.unmodifiableList(entryList);
return entryList;
}
protected abstract static class DBLoader extends AsyncTask> {
protected final WeakReference> weakProvider;
public DBLoader(DBProvider provider) {
super();
weakProvider = new WeakReference<>(provider);
}
@Nullable
protected Context getContext() {
DBProvider provider = weakProvider.get();
return provider != null ? provider.context : null;
}
@WorkerThread
@Override
protected List doInBackground(Void param) {
Context ctx = getContext();
if (ctx == null)
return null;
DataHandler dataHandler = TBApplication.getApplication(ctx).getDataHandler();
return getEntryItems(dataHandler);
}
@WorkerThread
abstract List getEntryItems(DataHandler dataHandler);
@MainThread
@Override
protected void onPostExecute(List entryItems) {
DBProvider provider = weakProvider.get();
if (entryItems == null || provider == null || provider.mLoadTask != this)
return;
// get the result
provider.entryList = entryItems;
// mark the provider as loaded
provider.setLoaded();
provider.mLoadTask = null;
provider.mTimer.stop();
Log.i("time", "Time to load " + provider.getClass().getSimpleName() + ": " + provider.mTimer);
DataHandler.sendBroadcast(provider.context, TBLauncherActivity.LOAD_OVER, provider.getClass().getSimpleName());
}
public void execute() {
TaskRunner.executeOnExecutor(DataHandler.EXECUTOR_PROVIDERS, this);
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/DialProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import androidx.annotation.NonNull;
import java.util.regex.Pattern;
import rocks.tbog.tblauncher.entry.ContactEntry;
import rocks.tbog.tblauncher.entry.DialContactEntry;
import rocks.tbog.tblauncher.searcher.ISearcher;
public class DialProvider extends SimpleProvider {
// See https://github.com/Neamar/KISS/issues/1137
private final Pattern phonePattern;
private final DialContactEntry resultEntry;
public DialProvider() {
phonePattern = Pattern.compile("^[*+0-9# ]{3,}$");
resultEntry = new DialContactEntry();
}
@Override
public boolean mayFindById(@NonNull String id) {
return id.startsWith(DialContactEntry.SCHEME);
}
@Override
public DialContactEntry findById(@NonNull String id) {
if (resultEntry.id.equals(id))
return resultEntry;
return null;
}
@Override
public void requestResults(String query, ISearcher searcher) {
// Append an item only if query looks like a phone number and device has phone capabilities
if (phonePattern.matcher(query).find()) {
searcher.addResult(getResult(query));
}
}
/**
* @param phoneNumber phone number to use in the result
* @return a result that may have a fake id.
*/
private ContactEntry getResult(String phoneNumber) {
DialContactEntry pojo = resultEntry;
pojo.setPhone(phoneNumber);
pojo.setName(phoneNumber, false);
pojo.setRelevance(pojo.normalizedName, null);
String phoneNumberAfterFirstCharacter = phoneNumber.substring(1);
if (!phoneNumberAfterFirstCharacter.contains("*") && !phoneNumberAfterFirstCharacter.contains("+")) {
// No * and no + (except maybe as a first character), likely to be a phone number and not a Calculator expression
pojo.boostRelevance(20);
} else {
// Query may be a phone number or a calculator expression, more likely to be an expression
// Calculator expressions have a relevance of 19, so use something lower
pojo.boostRelevance(15);
}
return pojo;
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/EntryToResultUtils.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import android.util.Log;
import androidx.annotation.WorkerThread;
import java.util.Collection;
import rocks.tbog.tblauncher.entry.EntryItem;
import rocks.tbog.tblauncher.entry.EntryWithTags;
import rocks.tbog.tblauncher.normalizer.StringNormalizer;
import rocks.tbog.tblauncher.searcher.ISearcher;
import rocks.tbog.tblauncher.searcher.ResultBuffer;
import rocks.tbog.tblauncher.utils.FuzzyScore;
public class EntryToResultUtils {
final static String TAG = "E2R";
interface CheckResults {
void checkResults(Collection entries, FuzzyScore fuzzyScore, ISearcher searcher);
}
@WorkerThread
public static void recursiveWordCheck(Collection entries, String query, ISearcher searcher, CheckResults action, Class typeClass) {
int pos = query.lastIndexOf(' ');
if (pos > 0) {
String queryLeft = query.substring(0, pos).trim();
String queryRight = query.substring(pos + 1).trim();
StringNormalizer.Result queryNormalizedRight = StringNormalizer.normalizeWithResult(queryRight, false);
if (queryNormalizedRight.codePoints.length > 0) {
ResultBuffer buffer = new ResultBuffer<>(searcher.tagsEnabled(), typeClass);
recursiveWordCheck(entries, queryLeft, buffer, action, typeClass);
FuzzyScore fuzzyScoreRight = new FuzzyScore(queryNormalizedRight.codePoints);
action.checkResults(buffer.getEntryItems(), fuzzyScoreRight, searcher);
return;
}
}
StringNormalizer.Result queryNormalized = StringNormalizer.normalizeWithResult(query, false);
if (queryNormalized.codePoints.length == 0)
return;
FuzzyScore fuzzyScore = new FuzzyScore(queryNormalized.codePoints);
action.checkResults(entries, fuzzyScore, searcher);
}
@WorkerThread
public static void tagsCheckResults(Collection extends EntryWithTags> entries, FuzzyScore fuzzyScore, ISearcher searcher) {
Log.d(TAG, "tagsCheckResults count=" + entries.size() + " " + fuzzyScore);
for (EntryWithTags entry : entries) {
if (entry.isHiddenByUser()) {
continue;
}
FuzzyScore.MatchInfo scoreInfo = fuzzyScore.match(entry.normalizedName.codePoints);
StringNormalizer.Result matchedText = entry.normalizedName;
FuzzyScore.MatchInfo matchedInfo = FuzzyScore.MatchInfo.copyOrNewInstance(scoreInfo, null);
if (searcher.tagsEnabled()) {
// check relevance for tags
for (EntryWithTags.TagDetails tag : entry.getTags()) {
// fuzzyScore.match will return the same object
scoreInfo = fuzzyScore.match(tag.normalized.codePoints);
if (scoreInfo.match && (!matchedInfo.match || scoreInfo.score > matchedInfo.score)) {
matchedText = tag.normalized;
matchedInfo = FuzzyScore.MatchInfo.copyOrNewInstance(scoreInfo, matchedInfo);
}
}
}
entry.addResultMatch(matchedText, matchedInfo);
if (matchedInfo.match && !searcher.addResult(entry)) {
return;
}
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/FilterProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import rocks.tbog.tblauncher.R;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.entry.AppEntry;
import rocks.tbog.tblauncher.entry.ContactEntry;
import rocks.tbog.tblauncher.entry.FilterEntry;
import rocks.tbog.tblauncher.entry.ShortcutEntry;
public class FilterProvider extends DBProvider {
private static final FilterEntry[] s_entries = new FilterEntry[3];
@StringRes
private static final int[] s_names = new int[3];
static {
int cnt = 0;
// apps filter
{
String id = FilterEntry.SCHEME + "applications";
FilterEntry filter = new FilterEntry(id, R.drawable.ic_apps, AppEntry.SCHEME);
filter.setOnClickListener(v -> {
Context ctx = v.getContext();
AppProvider provider = TBApplication.getApplication(ctx).getDataHandler().getAppProvider();
TBApplication.quickList(ctx).toggleFilter(v, provider);
});
s_names[cnt] = R.string.filter_apps;
s_entries[cnt++] = filter;
}
// contacts filter
{
String id = FilterEntry.SCHEME + "contacts";
FilterEntry filter = new FilterEntry(id, R.drawable.ic_contacts, ContactEntry.SCHEME);
filter.setOnClickListener(v -> {
Context ctx = v.getContext();
ContactsProvider provider = TBApplication.dataHandler(ctx).getContactsProvider();
TBApplication.quickList(ctx).toggleFilter(v, provider);
});
s_names[cnt] = R.string.filter_contacts;
s_entries[cnt++] = filter;
}
// pinned shortcuts filter
{
String id = FilterEntry.SCHEME + "shortcuts";
FilterEntry filter = new FilterEntry(id, R.drawable.ic_shortcuts, ShortcutEntry.SCHEME);
filter.setOnClickListener(v -> {
Context ctx = v.getContext();
ShortcutsProvider provider = TBApplication.dataHandler(ctx).getShortcutsProvider();
TBApplication.quickList(ctx).toggleFilter(v, provider);
});
s_names[cnt] = R.string.filter_shortcuts;
s_entries[cnt++] = filter;
}
//noinspection ConstantConditions
if (cnt != s_entries.length || cnt != s_names.length)
throw new IllegalStateException("FilterEntry static list size");
}
public FilterProvider(Context context) {
super(context);
}
@Override
protected DBLoader newLoadTask() {
return new UpdateFromModsLoader<>(this, s_entries, s_names);
}
@Override
public boolean mayFindById(@NonNull String id) {
return id.startsWith(FilterEntry.SCHEME);
}
@NonNull
public String getDefaultName(@NonNull String id) {
for (int idx = 0; idx < s_entries.length; idx += 1) {
if (id.equals(s_entries[idx].id))
return context.getString(s_names[idx]);
}
return "null";
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/IProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import java.util.List;
import rocks.tbog.tblauncher.entry.EntryItem;
import rocks.tbog.tblauncher.searcher.ISearcher;
import rocks.tbog.tblauncher.utils.Timer;
/**
* Public interface exposed by every KISS data provider
*/
public interface IProvider {
int LOAD_STEP_1 = 0;
int LOAD_STEP_2 = 1;
int LOAD_STEP_3 = 2;
int[] LOAD_STEPS = new int[] {LOAD_STEP_1, LOAD_STEP_2, LOAD_STEP_3};
/**
* Post search results for the given query string to the searcher
* @param query Some string query (usually provided by the user)
* @param searcher The receiver of results
*/
@WorkerThread
void requestResults(String query, ISearcher searcher);
/**
* Reload the data stored in this provider
*
* `LOAD_OVER` will be emitted once the reload is complete. The data provider
* will stay usable (using it's old data) during the reload.
* @param cancelCurrentLoadTask pass true to stop current loading task and start another;
* pass false to do nothing if already loading
*/
void reload(boolean cancelCurrentLoadTask);
/**
* Indicate whether this provider has already loaded it's data
*
* If this method returns `false` then the client may listen for the
* `LOAD_OVER` intent for notification of when the provider is ready.
*
* @return Is the provider ready to process search results?
*/
boolean isLoaded();
/**
* User for debug, this is the last load duration
*
* @return amount of time it took for this provider to load. null if
*/
@Nullable
Timer getLoadDuration();
/**
* Indicate that some providers have reloaded and this one may need to also reload
*/
void setDirty();
/**
* Return the loading step for this provider
* @return one of the LOAD_STEPS
*/
int getLoadStep();
/**
* Tells whether or not this provider may be able to find the pojo with
* specified id
*
* @param id id we're looking for
* @return true if the provider can handle the query ; does not guarantee it
* will!
*/
boolean mayFindById(@NonNull String id);
/**
* Try to find a record by its id
*
* @param id id we're looking for
* @return null if not found
*/
T findById(@NonNull String id);
/**
* Get a list of all pojos, do not modify this list!
*
* @return list of all entries
*/
@Nullable
List getPojos();
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/ModProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
import rocks.tbog.tblauncher.db.ModRecord;
import rocks.tbog.tblauncher.entry.EntryItem;
import rocks.tbog.tblauncher.entry.ICustomIconEntry;
import rocks.tbog.tblauncher.handler.DataHandler;
/**
* This provider is loaded last and is responsible for setting custom names and icons
*/
public class ModProvider extends DBProvider {
public ModProvider(Context context) {
super(context);
}
@Override
public int getLoadStep() {
return LOAD_STEP_3;
}
@Override
protected DBLoader newLoadTask() {
return new FavLoader(this);
}
private static class FavLoader extends DBProvider.DBLoader {
public FavLoader(DBProvider provider) {
super(provider);
}
@Override
List getEntryItems(DataHandler dataHandler) {
List list = dataHandler.getMods();
ArrayList favList = new ArrayList<>(list.size());
// get EntryItem from ModRecord
for (ModRecord fav : list) {
EntryItem entry = dataHandler.getPojo(fav.record);
if (entry == null)
continue;
else if (entry instanceof ICustomIconEntry) {
if (fav.hasCustomIcon() && !((ICustomIconEntry) entry).hasCustomIcon())
((ICustomIconEntry) entry).setCustomIcon();
}
if (fav.hasCustomName())
entry.setName(fav.displayName);
favList.add(entry);
}
return favList;
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/Provider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import rocks.tbog.tblauncher.BuildConfig;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.TBLauncherActivity;
import rocks.tbog.tblauncher.entry.EntryItem;
import rocks.tbog.tblauncher.handler.DataHandler;
import rocks.tbog.tblauncher.loader.LoadEntryItem;
import rocks.tbog.tblauncher.utils.Timer;
public abstract class Provider extends Service implements IProvider {
final static String TAG = "Provider";
/**
* Binder given to clients
*/
private final IBinder binder = new LocalBinder();
/**
* Storage for search items used by this provider
*/
protected List pojos = Collections.emptyList();
private boolean loaded = false;
private LoadEntryItem loader = null;
/**
* Scheme used to build ids for the pojos created by this provider
*/
@NonNull
private String pojoScheme = "(none)://";
private final Timer mTimer = new Timer();
/**
* (Re-)load the providers resources when the provider has been completely initialized
* by the Android system
*/
@Override
public void onCreate() {
super.onCreate();
TBApplication.dataHandler(this).onProviderRecreated(this);
this.reload(true);
}
protected boolean isLoading() {
return loader != null;
}
protected void initialize(@NonNull LoadEntryItem loader) {
mTimer.start();
if (this.loader != null)
this.loader.cancel(false);
Log.i(TAG, "Starting provider: " + this.getClass().getSimpleName());
loader.setProvider(this);
this.loader = loader;
this.pojoScheme = loader.getScheme();
this.loader.execute();
}
public void reload(boolean cancelCurrentLoadTask) {
if (!cancelCurrentLoadTask && loader != null)
return;
loaded = false;
// Handled at subclass level
if (pojos.size() > 0) {
Log.v(TAG, "Reloading provider: " + this.getClass().getSimpleName());
}
}
@Override
public void setDirty() {
// do nothing, we don't depend on any other provider
}
@Override
public boolean isLoaded() {
return this.loaded;
}
@Nullable
@Override
public Timer getLoadDuration() {
return mTimer;
}
@Override
public int getLoadStep() {
return LOAD_STEP_1;
}
public void loadOver(ArrayList results) {
mTimer.stop();
Log.i(TAG, "Time to load " + this.getClass().getSimpleName() + ": " + mTimer);
// Store results
this.pojos = results;
this.loaded = true;
this.loader = null;
// Broadcast this event
DataHandler.sendBroadcast(this, TBLauncherActivity.LOAD_OVER, getClass().getSimpleName());
}
@NonNull
public String getScheme() {
return pojoScheme;
}
/**
* Tells whether or not this provider may be able to find the pojo with
* specified id
*
* @param id id we're looking for
* @return true if the provider can handle the query ; does not guarantee it
* will!
*/
public boolean mayFindById(@NonNull String id) {
return id.startsWith(pojoScheme);
}
/**
* Try to find a record by its id
*
* @param id id we're looking for
* @return null if not found
*/
public T findById(@NonNull String id) {
for (T pojo : pojos) {
if (pojo.id.equals(id)) {
return pojo;
}
}
return null;
}
@Nullable
@Override
public List getPojos() {
if (BuildConfig.DEBUG)
return Collections.unmodifiableList(pojos);
return pojos;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// We want this service to continue running until it is explicitly
// stopped, so return sticky.
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return this.binder;
}
/**
* Class used for the client Binder. Because we know this service always
* runs in the same process as its clients, we don't need to deal with IPC.
*/
public class LocalBinder extends Binder {
public IProvider getService() {
// Return this instance of the provider so that clients can call public methods
return Provider.this;
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/QuickListProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import android.content.Context;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.TBLauncherActivity;
import rocks.tbog.tblauncher.db.DBHelper;
import rocks.tbog.tblauncher.db.ModRecord;
import rocks.tbog.tblauncher.entry.EntryItem;
import rocks.tbog.tblauncher.entry.PlaceholderEntry;
import rocks.tbog.tblauncher.handler.DataHandler;
public class QuickListProvider extends DBProvider {
private final static String TAG = QuickListProvider.class.getSimpleName();
public QuickListProvider(Context context) {
super(context);
}
@Override
public int getLoadStep() {
return LOAD_STEP_1;
}
// @Override
// public List getPojos() {
// boolean needsSorting = false;
//
// Collection recordIds = mQuickListFavRecords.values();
// boolean remakeEntries = false;
// if (entryList.size() == recordIds.size()) {
// for (EntryItem entryItem : entryList) {
// if (!mQuickListFavRecords.containsKey(entryItem.id)) {
// Log.d(TAG, "remake: not found " + entryItem.id);
// remakeEntries = true;
// break;
// }
// }
// } else {
// Log.d(TAG, "remake: " + entryList.size() + " \u2260 " + recordIds.size());
// remakeEntries = true;
// }
// if (remakeEntries) {
// needsSorting = true;
// entryList.clear();
// // make them all placeholders, we'll replace later
// for (ModRecord fav : recordIds) {
// PlaceholderEntry entry = new PlaceholderEntry(fav.record, fav.position);
// entry.setName(fav.displayName);
// if (fav.hasCustomIcon())
// entry.setCustomIcon();
// entryList.add(entry);
// }
// }
//
// ArrayList toAdd = new ArrayList<>();
// DataHandler dataHandler = TBApplication.dataHandler(context);
//
// // replace placeholders with the correct entry
// for (Iterator iterator = entryList.iterator(); iterator.hasNext(); ) {
// EntryItem entryItem = iterator.next();
// if (entryItem instanceof PlaceholderEntry) {
// needsSorting = true;
// entryItem = dataHandler.getPojo(entryItem.id);
// if (entryItem != null) {
// toAdd.add(entryItem);
// iterator.remove();
// }
// }
// }
// entryList.addAll(toAdd);
// // if we have replaced some PlaceholderEntry then we need to sort again
// if (needsSorting) {
// // sort entryList
// Collections.sort(entryList, (o1, o2) -> {
// ModRecord p1 = mQuickListFavRecords.get(o1.id);
// ModRecord p2 = mQuickListFavRecords.get(o2.id);
// if (p1 == null || p1.position == null || p2 == null || p2.position == null)
// return 0;
// return p1.position.compareTo(p2.position);
// });
// }
// return super.getPojos();
// }
private void fixPlaceholders() {
DataHandler dataHandler = TBApplication.dataHandler(context);
int replaceCount = 0;
for (int idx = 0; idx < entryList.size(); idx += 1) {
EntryItem entryItem = entryList.get(idx);
if (entryItem instanceof PlaceholderEntry) {
entryItem = dataHandler.getPojo(entryItem.id);
if (entryItem != null) {
entryList.set(idx, entryItem);
replaceCount += 1;
}
}
}
Log.i(TAG, "replaced " + replaceCount + "/" + entryList.size() + " placeholder(s)");
}
@Override
protected DBLoader newLoadTask() {
return new QuickListLoader(this);
}
private static class QuickListLoader extends DBProvider.DBLoader {
public QuickListLoader(DBProvider provider) {
super(provider);
}
@Override
List getEntryItems(DataHandler dataHandler) {
Context context = getContext();
if (context == null)
return null;
ArrayList records = DBHelper.getMods(context);
int quickListSize = 0;
// count only items in the QuickList
for (ModRecord rec : records) {
if (rec.isInQuickList())
quickListSize += 1;
}
ArrayList quickList = new ArrayList<>(quickListSize);
// get EntryItem from ModRecord
for (ModRecord fav : records) {
if (!fav.isInQuickList())
continue;
PlaceholderEntry entry = new PlaceholderEntry(fav.record, fav.position);
entry.setName(fav.displayName);
if (fav.hasCustomIcon())
entry.setCustomIcon();
quickList.add(entry);
}
Collections.sort(quickList, (o1, o2) -> {
String p1 = ((PlaceholderEntry) o1).position;
String p2 = ((PlaceholderEntry) o2).position;
if (p1 == null || p2 == null)
return 0;
return p1.compareTo(p2);
});
return quickList;
}
@Override
protected void onPostExecute(List entryItems) {
super.onPostExecute(entryItems);
Context context = getContext();
if (context == null)
return;
TBApplication.dataHandler(context).runAfterLoadOver(() -> {
DBProvider provider = weakProvider.get();
if (provider instanceof QuickListProvider) {
((QuickListProvider) provider).fixPlaceholders();
TBLauncherActivity launcherActivity = TBApplication.launcherActivity(provider.context);
if (launcherActivity != null)
launcherActivity.queueDockReload();
}
});
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/SearchProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import android.content.Context;
import android.content.SharedPreferences;
import android.webkit.URLUtil;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import rocks.tbog.tblauncher.BuildConfig;
import rocks.tbog.tblauncher.R;
import rocks.tbog.tblauncher.entry.OpenUrlEntry;
import rocks.tbog.tblauncher.entry.SearchEngineEntry;
import rocks.tbog.tblauncher.entry.SearchEntry;
import rocks.tbog.tblauncher.normalizer.StringNormalizer;
import rocks.tbog.tblauncher.searcher.ISearcher;
import rocks.tbog.tblauncher.utils.FuzzyScore;
public class SearchProvider extends SimpleProvider {
private static final String URL_REGEX = "^(?:[a-z]+://)?(?:[a-z0-9-]|[^\\x00-\\x7F])+(?:[.](?:[a-z0-9-]|[^\\x00-\\x7F])+)+.*$";
public static final Pattern urlPattern = Pattern.compile(URL_REGEX);
private final SharedPreferences prefs;
private final ArrayList searchEngines = new ArrayList<>();
private final Context context;
@NonNull
public static Set getDefaultSearchProviders(Context context) {
String[] defaultSearchProviders = context.getResources().getStringArray(R.array.defaultSearchProviders);
return new ArraySet<>(Arrays.asList(defaultSearchProviders));
}
@NonNull
public static Set getAvailableSearchProviders(Context context, SharedPreferences prefs) {
Set availableProviders = prefs.getStringSet("available-search-providers", null);
if (availableProviders == null)
availableProviders = SearchProvider.getDefaultSearchProviders(context);
if (BuildConfig.DEBUG)
return Collections.unmodifiableSet(availableProviders);
return availableProviders;
}
@NonNull
public static Set getSelectedProviderNames(Context context, SharedPreferences prefs) {
Set selectedProviders = prefs.getStringSet("selected-search-provider-names", null);
if (selectedProviders == null) {
Set availableProviders = getAvailableSearchProviders(context, prefs);
selectedProviders = new ArraySet<>(availableProviders.size());
for (String availableProvider : availableProviders)
selectedProviders.add(getProviderName(availableProvider));
}
return selectedProviders;
}
@NonNull
public static String sanitizeProviderName(@Nullable String name) {
if (name == null)
return "[name]";
while (name.contains("|"))
name = name.replace('|', ' ');
return name;
}
@NonNull
public static String sanitizeProviderUrl(@Nullable String url) {
if (url == null)
return "%s";
if (!url.contains("%s"))
return url + "%s";
return url;
}
public SearchProvider(Context context, SharedPreferences sharedPreferences) {
super();
this.context = context.getApplicationContext();
this.prefs = sharedPreferences;
reload(false);
}
@Override
public void reload(boolean cancelCurrentLoadTask) {
searchEngines.clear();
Set availableSearchProviders = SearchProvider.getAvailableSearchProviders(context, prefs);
Set selectedProviderNames = SearchProvider.getSelectedProviderNames(context, prefs);
for (String searchProvider : availableSearchProviders) {
String name = getProviderName(searchProvider);
if (selectedProviderNames.contains(name)) {
String url = getProviderUrl(searchProvider);
SearchEngineEntry entry = new SearchEngineEntry(name, url);
if (url != null)
searchEngines.add(entry);
}
}
}
@Override
public boolean mayFindById(@NonNull String id) {
return id.startsWith(SearchEngineEntry.SCHEME);
}
@Override
public SearchEntry findById(@NonNull String id) {
for (SearchEngineEntry entry : searchEngines)
if (entry.id.equals(id))
return entry;
return null;
}
@Override
public void requestResults(String query, ISearcher searcher) {
searcher.addResult(getResults(query).toArray(new SearchEntry[0]));
}
@NonNull
private ArrayList getResults(String query) {
ArrayList records = new ArrayList<>();
StringNormalizer.Result queryNormalized = StringNormalizer.normalizeWithResult(query, false);
if (queryNormalized.codePoints.length == 0) {
return records;
}
if (prefs.getBoolean("enable-search", true)) {
// Get default search engine
String defaultSearchEngine = prefs.getString("default-search-provider", "Google");
for (SearchEngineEntry entry : searchEngines) {
entry.setQuery(query);
entry.setRelevance(entry.normalizedName, null);
// Super low relevance, should never be displayed before anything
entry.boostRelevance(-500);
if (entry.getName().equals(defaultSearchEngine))
// Display default search engine slightly higher
entry.boostRelevance(100);
records.add(entry);
}
}
if (prefs.getBoolean("enable-url", true)) {
FuzzyScore fuzzyScore = new FuzzyScore(queryNormalized.codePoints);
// Open URLs directly (if I type http://something.com for instance)
Matcher m = urlPattern.matcher(query);
if (m.find()) {
String guessedUrl = URLUtil.guessUrl(query);
if (URLUtil.isHttpUrl(guessedUrl))
guessedUrl = "https://" + guessedUrl.substring(7);
if (URLUtil.isValidUrl(guessedUrl)) {
SearchEntry pojo = new OpenUrlEntry(query, guessedUrl);
pojo.setName(guessedUrl);
FuzzyScore.MatchInfo matchInfo = fuzzyScore.match(pojo.normalizedName.codePoints);
pojo.setRelevance(pojo.normalizedName, matchInfo);
records.add(pojo);
}
}
}
return records;
}
@Nullable
public static String getProviderUrl(@NonNull String searchProvider) {
int pos = searchProvider.indexOf("|");
if (pos >= 0)
return searchProvider.substring(pos + 1);
if (URLUtil.isValidUrl(searchProvider))
return searchProvider;
return null;
}
@NonNull
public static String getProviderName(@NonNull String searchProvider) {
int pos = searchProvider.indexOf("|");
if (pos >= 0)
return searchProvider.substring(0, pos);
return "null";
}
@NonNull
public static String makeProvider(@NonNull String name, @NonNull String url) {
return sanitizeProviderName(name) + "|" + sanitizeProviderUrl(url);
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/ShortcutsProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Parcelable;
import android.os.UserHandle;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.util.List;
import rocks.tbog.tblauncher.R;
import rocks.tbog.tblauncher.TBApplication;
import rocks.tbog.tblauncher.db.ShortcutRecord;
import rocks.tbog.tblauncher.entry.ShortcutEntry;
import rocks.tbog.tblauncher.loader.LoadShortcutsEntryItem;
import rocks.tbog.tblauncher.searcher.ISearcher;
import rocks.tbog.tblauncher.shortcut.ShortcutUtil;
import rocks.tbog.tblauncher.utils.Utilities;
public class ShortcutsProvider extends Provider {
private static boolean notifiedKissNotDefaultLauncher = false;
private static final String ACTION_INSTALL_SHORTCUT = "com.android.launcher.action.INSTALL_SHORTCUT";
AppsCallback appsCallback = null;
final BroadcastReceiver mProfileReceiver = new BroadcastReceiver() {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_MANAGED_PROFILE_ADDED.equals(intent.getAction())) {
// ShortcutsProvider.this.reload();
} else if (Intent.ACTION_MANAGED_PROFILE_REMOVED.equals(intent.getAction())) {
// android.os.UserHandle profile = intent.getParcelableExtra(Intent.EXTRA_USER);
//
// final UserManager manager = (UserManager) ShortcutsProvider.this.getSystemService(Context.USER_SERVICE);
// assert manager != null;
// UserHandleCompat user = new UserHandleCompat(manager.getSerialNumberForUser(profile), profile);
//
// DataHandler dataHandler = TBApplication.getApplication(context).getDataHandler();
// dataHandler.removeFromExcluded(user);
// dataHandler.removeFromFavorites(user);
// ShortcutsProvider.this.reload();
} else if (ACTION_INSTALL_SHORTCUT.equals(intent.getAction())) {
Intent i = intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
String name = intent.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
Parcelable bitmap = intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
if (i == null) {
Log.w("SHC", "Shortcut intent is null " + name);
return;
}
Drawable icon = null;
if (bitmap instanceof Bitmap) {
icon = Utilities.createIconDrawable((Bitmap) bitmap, context);
} else {
Parcelable extra = intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
if (extra instanceof Intent.ShortcutIconResource) {
icon = Utilities.createIconDrawable((Intent.ShortcutIconResource) extra, context);
}
}
if (icon == null) {
icon = TBApplication.getApplication(context).iconsHandler().getDefaultActivityIcon(context);
}
ShortcutRecord record = new ShortcutRecord();
record.displayName = name;
record.infoData = i.toUri(Intent.URI_INTENT_SCHEME);
record.iconPng = ShortcutUtil.getIconBlob(icon);
record.packageName = i.getPackage();
if (record.packageName == null)
record.packageName = i.getComponent() != null ? i.getComponent().getPackageName() : "";
if (!TBApplication.getApplication(context).getDataHandler().addShortcut(record))
Log.w("SHC", "Failed to add shortcut " + name);
}
}
};
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
static class AppsCallback extends LauncherApps.Callback {
private final Context context;
AppsCallback(Context context) {
this.context = context;
}
@Override
public void onPackageRemoved(String packageName, UserHandle user) {
}
@Override
public void onPackageAdded(String packageName, UserHandle user) {
}
@Override
public void onPackageChanged(String packageName, UserHandle user) {
}
@Override
public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) {
}
@Override
public void onPackagesUnavailable(String[] packageNames, UserHandle user, boolean replacing) {
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public void onShortcutsChanged(@NonNull String packageName, @NonNull List shortcuts, @NonNull UserHandle user) {
super.onShortcutsChanged(packageName, shortcuts, user);
for (ShortcutInfo info : shortcuts) {
String action = null;
ComponentName component = null;
String intentPackage = null;
Intent i = info.getIntent();
if (i != null) {
action = i.getAction();
component = i.getComponent();
intentPackage = i.getPackage();
}
Log.i("SHC", "Shortcut changed for `" + packageName + "`" +
"\naction " + action +
"\ncomponent " + component +
"\nintentPack " + intentPackage +
"\nshortLabel " + info.getShortLabel() +
"\nlongLabel " + info.getLongLabel() +
"\nisImmutable " + info.isImmutable() +
"\nisEnabled " + info.isEnabled() +
"\nisPinned " + info.isPinned() +
"\ninManifest " + info.isDeclaredInManifest() +
"\nisDynamic " + info.isDynamic());
}
}
}
@Override
public void onCreate() {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Package install/uninstall events for the main
// profile are still handled using PackageAddedRemovedHandler itself
// final LauncherApps launcher = (LauncherApps) this.getSystemService(Context.LAUNCHER_APPS_SERVICE);
// assert launcher != null;
//
// appsCallback = new AppsCallback(this);
// launcher.registerCallback(appsCallback);
// Try to clean up app-related data when profile is removed
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
filter.addAction(ACTION_INSTALL_SHORTCUT);
ActivityCompat.registerReceiver(this, mProfileReceiver, filter, ContextCompat.RECEIVER_EXPORTED);
}
super.onCreate();
}
@Override
public void onDestroy() {
unregisterReceiver(mProfileReceiver);
if (appsCallback != null) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
LauncherApps launcher = (LauncherApps) this.getSystemService(Context.LAUNCHER_APPS_SERVICE);
assert launcher != null;
launcher.unregisterCallback(appsCallback);
}
}
super.onDestroy();
}
@Override
public void reload(boolean cancelCurrentLoadTask) {
super.reload(cancelCurrentLoadTask);
if (!isLoaded() && !isLoading()) {
try {
// If the user tries to add a new shortcut, but KISS isn't the default launcher
// AND the services are not running (low memory), then we won't be able to
// spawn a new service on Android 8.1+.
this.initialize(new LoadShortcutsEntryItem(this));
} catch (IllegalStateException e) {
if (!notifiedKissNotDefaultLauncher) {
// Only display this message once per process
Toast.makeText(this, R.string.unable_to_initialize_shortcuts, Toast.LENGTH_LONG).show();
}
notifiedKissNotDefaultLauncher = true;
e.printStackTrace();
}
}
}
@WorkerThread
@Override
public void requestResults(String query, ISearcher searcher) {
for (ShortcutEntry pojo : pojos)
pojo.resetResultInfo();
EntryToResultUtils.recursiveWordCheck(pojos, query, searcher, EntryToResultUtils::tagsCheckResults, ShortcutEntry.class);
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/SimpleProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import rocks.tbog.tblauncher.entry.EntryItem;
import rocks.tbog.tblauncher.searcher.ISearcher;
import rocks.tbog.tblauncher.utils.Timer;
/**
* Unlike normal providers, simple providers are not Android Services but classic Android class
* Android Services are expensive to create, and use a lot of memory,
* so whenever we can, we avoid using them.
*/
public abstract class SimpleProvider implements IProvider {
@Override
public void requestResults(String query, ISearcher searcher) {
}
@Override
public void reload(boolean cancelCurrentLoadTask) {
}
@Override
public final boolean isLoaded() {
return true;
}
@Nullable
@Override
public Timer getLoadDuration() {
return null;
}
@Override
public void setDirty() {
}
@Override
public int getLoadStep() {
return LOAD_STEP_1;
}
@Override
public boolean mayFindById(@NonNull String id) {
return false;
}
@Override
public T findById(@NonNull String id) {
return null;
}
@Nullable
@Override
public List getPojos() {
return null;
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/TagsProvider.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import rocks.tbog.tblauncher.db.ModRecord;
import rocks.tbog.tblauncher.entry.TagEntry;
import rocks.tbog.tblauncher.handler.DataHandler;
public class TagsProvider extends DBProvider {
public TagsProvider(Context context) {
super(context);
}
@Override
protected DBLoader newLoadTask() {
return new FavLoader(this);
}
@Nullable
public static TagEntry newTagEntryCheckId(String id) {
if (id.startsWith(TagEntry.SCHEME)) {
TagEntry tagEntry = new TagEntry(id);
String tagName = id.substring(TagEntry.SCHEME.length());
tagEntry.setName(tagName);
return tagEntry;
}
return null;
}
@NonNull
public static String getTagId(@NonNull String tagName) {
return TagEntry.SCHEME + tagName;
}
@NonNull
private static TagEntry newTagEntry(@NonNull String id, @NonNull String tagName) {
TagEntry tagEntry = new TagEntry(id);
tagEntry.setName(tagName);
return tagEntry;
}
@Override
public boolean mayFindById(@NonNull String id) {
return id.startsWith(TagEntry.SCHEME);
}
@NonNull
public TagEntry getTagEntry(String tagName) {
String id = getTagId(tagName);
TagEntry entryItem = findById(id);
if (entryItem == null)
return newTagEntry(id, tagName);
return entryItem;
}
public void addTagEntry(TagEntry tagEntry) {
if (null == findById(tagEntry.id))
entryList.add(tagEntry);
}
private static class FavLoader extends DBProvider.DBLoader {
public FavLoader(DBProvider provider) {
super(provider);
}
@Override
List getEntryItems(DataHandler dataHandler) {
ArrayList tagList = new ArrayList<>();
List mods = dataHandler.getMods();
// get TagEntry from ModRecord
for (ModRecord mod : mods) {
TagEntry entry = newTagEntryCheckId(mod.record);
if (entry == null)
continue;
if (mod.hasCustomIcon())
entry.setCustomIcon();
tagList.add(entry);
}
return tagList;
}
}
}
================================================
FILE: app/src/main/java/rocks/tbog/tblauncher/dataprovider/UpdateFromModsLoader.java
================================================
package rocks.tbog.tblauncher.dataprovider;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
import rocks.tbog.tblauncher.handler.DataHandler;
import rocks.tbog.tblauncher.db.ModRecord;
import rocks.tbog.tblauncher.entry.StaticEntry;
public class UpdateFromModsLoader