/* * Twidere - Twitter client for Android * * Copyright (C) 2012-2014 Mariotaku Lee * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.mariotaku.twidere.fragment import android.accounts.AccountManager import android.app.Dialog import android.content.ContentValues import android.content.Context import android.content.DialogInterface import android.content.Intent import android.database.Cursor import android.graphics.Paint import android.graphics.PorterDuff.Mode import android.os.Bundle import androidx.fragment.app.Fragment import androidx.loader.app.LoaderManager.LoaderCallbacks import androidx.loader.content.CursorLoader import androidx.loader.content.Loader import androidx.appcompat.app.AlertDialog import android.util.SparseArray import android.view.* import android.widget.* import android.widget.AbsListView.MultiChoiceModeListener import android.widget.AdapterView.OnItemClickListener import com.mobeta.android.dslv.SimpleDragSortCursorAdapter import kotlinx.android.synthetic.main.dialog_custom_tab_editor.* import kotlinx.android.synthetic.main.layout_draggable_list_with_empty_view.* import kotlinx.android.synthetic.main.list_item_section_header.view.* import org.mariotaku.chameleon.Chameleon import org.mariotaku.ktextension.Bundle import org.mariotaku.ktextension.contains import org.mariotaku.ktextension.set import org.mariotaku.ktextension.spannable import org.mariotaku.library.objectcursor.ObjectCursor import org.mariotaku.sqliteqb.library.Columns.Column import org.mariotaku.sqliteqb.library.Expression import org.mariotaku.sqliteqb.library.RawItemArray import org.mariotaku.twidere.R import org.mariotaku.twidere.TwidereConstants.* import org.mariotaku.twidere.activity.SettingsActivity import org.mariotaku.twidere.adapter.AccountsSpinnerAdapter import org.mariotaku.twidere.adapter.ArrayAdapter import org.mariotaku.twidere.annotation.CustomTabType import org.mariotaku.twidere.annotation.TabAccountFlags import org.mariotaku.twidere.extension.applyTheme import org.mariotaku.twidere.extension.model.isOfficial import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.Tab import org.mariotaku.twidere.model.tab.DrawableHolder import org.mariotaku.twidere.model.tab.TabConfiguration import org.mariotaku.twidere.model.tab.iface.AccountCallback import org.mariotaku.twidere.model.util.AccountUtils import org.mariotaku.twidere.provider.TwidereDataStore.Tabs import org.mariotaku.twidere.util.CustomTabUtils import org.mariotaku.twidere.util.ThemeUtils import org.mariotaku.twidere.view.holder.TwoLineWithIconViewHolder import java.lang.ref.WeakReference class CustomTabsFragment : BaseFragment(), LoaderCallbacks, MultiChoiceModeListener { private lateinit var adapter: CustomTabsAdapter override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { when (item.itemId) { R.id.delete -> { val itemIds = listView.checkedItemIds val where = Expression.`in`(Column(Tabs._ID), RawItemArray(itemIds)) context?.contentResolver?.delete(Tabs.CONTENT_URI, where.sql, null) activity?.let { SettingsActivity.setShouldRestart(it) } } } mode.finish() return true } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setHasOptionsMenu(true) adapter = CustomTabsAdapter(requireContext()) listView.choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL listView.setMultiChoiceModeListener(this) listView.onItemClickListener = OnItemClickListener { _, _, position, _ -> val tab = adapter.getTab(position) val df = TabEditorDialogFragment() df.arguments = Bundle { this[EXTRA_OBJECT] = tab } fragmentManager?.let { df.show(it, TabEditorDialogFragment.TAG_EDIT_TAB) } } listView.adapter = adapter listView.emptyView = emptyView listView.setDropListener { from, to -> adapter.drop(from, to) if (listView.choiceMode != AbsListView.CHOICE_MODE_NONE) { listView.moveCheckState(from, to) } saveTabPositions() } emptyText.setText(R.string.no_tab) emptyIcon.setImageResource(R.drawable.ic_info_tab) loaderManager.initLoader(0, null, this) setListShown(false) } private fun setListShown(shown: Boolean) { listContainer.visibility = if (shown) View.VISIBLE else View.GONE progressContainer.visibility = if (shown) View.GONE else View.VISIBLE } override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { mode.menuInflater.inflate(R.menu.action_multi_select_items, menu) return true } override fun onCreateLoader(id: Int, args: Bundle?): Loader { return CursorLoader(requireActivity(), Tabs.CONTENT_URI, Tabs.COLUMNS, null, null, Tabs.DEFAULT_SORT_ORDER) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.menu_custom_tabs, menu) val context = this.context val accounts = AccountUtils.getAllAccountDetails(AccountManager.get(context), false) val itemAdd = menu.findItem(R.id.add_submenu) val theme = Chameleon.getOverrideTheme(requireContext(), context) if (itemAdd != null && itemAdd.hasSubMenu()) { val subMenu = itemAdd.subMenu subMenu.clear() for ((type, conf) in TabConfiguration.all()) { val accountRequired = TabAccountFlags.FLAG_ACCOUNT_REQUIRED in conf.accountFlags val subItem = subMenu.add(0, 0, conf.sortPosition, conf.name.createString(context)) val disabledByNoAccount = accountRequired && accounts.none(conf::checkAccountAvailability) val disabledByDuplicateTab = conf.isSingleTab && CustomTabUtils.isTabAdded(context, type) val shouldDisable = disabledByDuplicateTab || disabledByNoAccount subItem.isVisible = !shouldDisable subItem.isEnabled = !shouldDisable val icon = conf.icon.createDrawable(context) icon.mutate().setColorFilter(theme.textColorPrimary, Mode.SRC_ATOP) subItem.icon = icon val weakFragment = WeakReference(this) subItem.setOnMenuItemClickListener { val fragment = weakFragment.get()?.takeUnless(Fragment::isDetached) ?: return@setOnMenuItemClickListener false val adapter = fragment.adapter val fm = fragment.childFragmentManager val df = TabEditorDialogFragment() df.arguments = Bundle { this[EXTRA_TAB_TYPE] = type if (!adapter.isEmpty) { this[EXTRA_TAB_POSITION] = adapter.getTab(adapter.count - 1).position + 1 } } df.show(fm, TabEditorDialogFragment.TAG_ADD_TAB) return@setOnMenuItemClickListener true } } } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.layout_draggable_list_with_empty_view, container, false) } override fun onDestroyActionMode(mode: ActionMode) { } override fun onItemCheckedStateChanged(mode: ActionMode, position: Int, id: Long, checked: Boolean) { updateTitle(mode) } override fun onLoaderReset(loader: Loader) { adapter.changeCursor(null) } override fun onLoadFinished(loader: Loader, cursor: Cursor?) { adapter.changeCursor(cursor) setListShown(true) } override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { updateTitle(mode) return true } private fun saveTabPositions() { val positions = adapter.cursorPositions val c = adapter.cursor if (positions != null && c != null && !c.isClosed) { val idIdx = c.getColumnIndex(Tabs._ID) for (i in 0 until positions.size) { c.moveToPosition(positions[i]) val id = c.getLong(idIdx) val values = ContentValues() values.put(Tabs.POSITION, i) val where = Expression.equals(Tabs._ID, id).sql context?.contentResolver?.update(Tabs.CONTENT_URI, values, where, null) } } activity?.let { SettingsActivity.setShouldRestart(it) } } private fun updateTitle(mode: ActionMode?) { if (listView == null || mode == null || activity == null) return val count = listView.checkedItemCount mode.title = resources.getQuantityString(R.plurals.Nitems_selected, count, count) } class TabEditorDialogFragment : BaseDialogFragment(), DialogInterface.OnShowListener, AccountCallback { private val activityResultMap: SparseArray = SparseArray() override fun onShow(dialogInterface: DialogInterface) { val currentContext = context ?: return val currentArguments = arguments ?: return val dialog = dialogInterface as AlertDialog dialog.applyTheme() @CustomTabType val tabType: String val tab: Tab val conf: TabConfiguration when (tag) { TAG_ADD_TAB -> { tabType = currentArguments.getString(EXTRA_TAB_TYPE)!! tab = Tab() conf = TabConfiguration.ofType(tabType)!! tab.type = tabType tab.icon = conf.icon.persistentKey tab.position = currentArguments.getInt(EXTRA_TAB_POSITION) } TAG_EDIT_TAB -> { tab = currentArguments.getParcelable(EXTRA_OBJECT)!! tabType = tab.type conf = TabConfiguration.ofType(tabType) ?: run { dismiss() return } } else -> { throw AssertionError() } } val tabName = dialog.tabName val iconSpinner = dialog.tabIconSpinner val accountSpinner = dialog.accountSpinner val accountContainer = dialog.accountContainer val extraConfigContainer = dialog.extraConfigContainer val positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE) val iconsAdapter = TabIconsAdapter(currentContext) val accountsAdapter = AccountsSpinnerAdapter(currentContext, requestManager = requestManager) iconSpinner.adapter = iconsAdapter accountSpinner.adapter = accountsAdapter iconsAdapter.setData(DrawableHolder.builtins()) tabName.hint = conf.name.createString(currentContext) tabName.setText(tab.name) iconSpinner.setSelection(iconsAdapter.findPositionByKey(tab.icon)) val editMode = tag == TAG_EDIT_TAB val hasAccount = TabAccountFlags.FLAG_HAS_ACCOUNT in conf.accountFlags val accountMutable = TabAccountFlags.FLAG_ACCOUNT_MUTABLE in conf.accountFlags if (hasAccount && (accountMutable || !editMode)) { accountContainer.visibility = View.VISIBLE val accountRequired = TabAccountFlags.FLAG_ACCOUNT_REQUIRED in conf.accountFlags accountsAdapter.clear() if (!accountRequired) { accountsAdapter.add(AccountDetails.dummy()) } val officialKeyOnly = arguments?.getBoolean(EXTRA_OFFICIAL_KEY_ONLY, false) ?: false accountsAdapter.addAll(AccountUtils.getAllAccountDetails(AccountManager.get(context), true).filter { if (officialKeyOnly && !it.isOfficial(context)) { return@filter false } return@filter conf.checkAccountAvailability(it) }) accountsAdapter.setDummyItemText(R.string.activated_accounts) tab.arguments?.accountKeys?.firstOrNull()?.let { key -> accountSpinner.setSelection(accountsAdapter.findPositionByKey(key)) } } else { accountContainer.visibility = View.GONE } val extraConfigurations = conf.getExtraConfigurations(currentContext).orEmpty() fun inflateHeader(title: String): View { val headerView = LayoutInflater.from(currentContext).inflate(R.layout.list_item_section_header, extraConfigContainer, false) headerView.sectionHeader.text = title return headerView } extraConfigurations.forEachIndexed { idx, extraConf -> extraConf.onCreate(currentContext) extraConf.position = idx + 1 // Hide immutable settings in edit mode if (editMode && !extraConf.isMutable) return@forEachIndexed extraConf.headerTitle?.let { // Inflate header with headerTitle extraConfigContainer.addView(inflateHeader(it.createString(currentContext))) } val view = extraConf.onCreateView(currentContext, extraConfigContainer) extraConf.onViewCreated(currentContext, view, this) conf.readExtraConfigurationFrom(tab, extraConf) extraConfigContainer.addView(view) } accountSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { private fun updateExtraTabs(account: AccountDetails?) { extraConfigurations.forEach { it.onAccountSelectionChanged(account) } } override fun onItemSelected(parent: AdapterView<*>, view: View, pos: Int, id: Long) { val account = parent.selectedItem as? AccountDetails updateExtraTabs(account) } override fun onNothingSelected(view: AdapterView<*>) { } } positiveButton.setOnClickListener { tab.name = tabName.text.toString() tab.icon = (iconSpinner.selectedItem as DrawableHolder).persistentKey if (tab.arguments == null) { tab.arguments = CustomTabUtils.newTabArguments(tabType) } if (tab.extras == null) { tab.extras = CustomTabUtils.newTabExtras(tabType) } if (hasAccount && (!editMode || TabAccountFlags.FLAG_ACCOUNT_MUTABLE in conf.accountFlags)) { val account = accountSpinner.selectedItem as? AccountDetails ?: return@setOnClickListener if (!account.dummy) { tab.arguments?.accountKeys = arrayOf(account.key) } else { tab.arguments?.accountKeys = null } } extraConfigurations.forEach { extraConf -> // Make sure immutable configuration skipped in edit mode if (editMode && !extraConf.isMutable) return@forEach if (!conf.applyExtraConfigurationTo(tab, extraConf)) { extraConf.showRequiredError() return@setOnClickListener } } val valuesCreator = ObjectCursor.valuesCreatorFrom(Tab::class.java) when (tag) { TAG_EDIT_TAB -> { val where = Expression.equals(Tabs._ID, tab.id).sql currentContext.contentResolver.update(Tabs.CONTENT_URI, valuesCreator.create(tab), where, null) } TAG_ADD_TAB -> { currentContext.contentResolver.insert(Tabs.CONTENT_URI, valuesCreator.create(tab)) } } activity?.let { it1 -> SettingsActivity.setShouldRestart(it1) } dismiss() } } override fun getAccount(): AccountDetails? { return dialog?.findViewById(R.id.accountSpinner)?.selectedItem as? AccountDetails } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val builder = AlertDialog.Builder(requireContext()) builder.setView(R.layout.dialog_custom_tab_editor) builder.setPositiveButton(R.string.action_save, null) builder.setNegativeButton(android.R.string.cancel, null) val dialog = builder.create() dialog.setOnShowListener(this) return dialog } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { val extraConf = activityResultMap.get(requestCode) activityResultMap.remove(requestCode) extraConf?.onActivityResult(this, requestCode and 0xFF, resultCode, data) } fun startExtraConfigurationActivityForResult(extraConf: TabConfiguration.ExtraConfiguration, intent: Intent, requestCode: Int) { val requestCodeInternal = extraConf.position shl 8 and 0xFF00 or (requestCode and 0xFF) activityResultMap.put(requestCodeInternal, extraConf) startActivityForResult(intent, requestCodeInternal) } companion object { const val TAG_EDIT_TAB = "edit_tab" const val TAG_ADD_TAB = "add_tab" } } internal class TabIconsAdapter(context: Context) : ArrayAdapter(context, R.layout.spinner_item_custom_tab_icon) { private val iconColor: Int = ThemeUtils.getThemeForegroundColor(context) init { setDropDownViewResource(R.layout.list_item_two_line_small) } override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { val view = super.getDropDownView(position, convertView, parent) view.findViewById(android.R.id.text2).visibility = View.GONE val text1 = view.findViewById(android.R.id.text1) val item = getItem(position) text1.spannable = item.name bindIconView(item, view) return view } override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val view = super.getView(position, convertView, parent) bindIconView(getItem(position), view) return view } fun setData(list: List?) { clear() if (list == null) return addAll(list) } private fun bindIconView(item: DrawableHolder, view: View) { val icon = view.findViewById(android.R.id.icon) icon.setColorFilter(iconColor, Mode.SRC_ATOP) icon.setImageDrawable(item.createDrawable(icon.context)) } fun findPositionByKey(key: String): Int { return (0 until count).indexOfFirst { getItem(it).persistentKey == key } } } class CustomTabsAdapter(context: Context) : SimpleDragSortCursorAdapter(context, R.layout.list_item_custom_tab, null, emptyArray(), intArrayOf(), 0) { private val iconColor: Int = ThemeUtils.getThemeForegroundColor(context) private val tempTab: Tab = Tab() private var indices: ObjectCursor.CursorIndices? = null override fun bindView(view: View, context: Context?, cursor: Cursor) { super.bindView(view, context, cursor) val holder = view.tag as TwoLineWithIconViewHolder indices?.parseFields(tempTab, cursor) val type = tempTab.type val name = tempTab.name val iconKey = tempTab.icon if (type != null && CustomTabUtils.isTabTypeValid(type)) { val typeName = CustomTabUtils.getTabTypeName(context, type) holder.text1.spannable = name?.takeIf(String::isNotEmpty) ?: typeName holder.text1.paintFlags = holder.text1.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() holder.text2.visibility = View.VISIBLE holder.text2.text = typeName } else { holder.text1.spannable = name holder.text1.paintFlags = holder.text1.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG holder.text2.setText(R.string.invalid_tab) } val icon = CustomTabUtils.getTabIconDrawable(context, DrawableHolder.parse(iconKey)) holder.icon.visibility = View.VISIBLE if (icon != null) { holder.icon.setImageDrawable(icon) } else { holder.icon.setImageResource(R.drawable.ic_action_list) } holder.icon.setColorFilter(iconColor, Mode.SRC_ATOP) } override fun changeCursor(cursor: Cursor?) { indices = cursor?.let { ObjectCursor.indicesFrom(it, Tab::class.java) } super.changeCursor(cursor) } override fun newView(context: Context?, cursor: Cursor?, parent: ViewGroup): View { val view = super.newView(context, cursor, parent) val tag = view.tag if (tag !is TwoLineWithIconViewHolder) { val holder = TwoLineWithIconViewHolder(view) view.tag = holder } return view } fun getTab(position: Int): Tab { cursor.moveToPosition(position) return indices!!.newObject(cursor) } } }