blob: 7c41e9b22fd9c7115e672658d2838e275eb6009d [file] [log] [blame]
/*
* Copyright (C) 2004-2019 Savoir-faire Linux Inc.
*
* Author: Adrien BĂ©raud <adrien.beraud@savoirfairelinux.com>s
*
* 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, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package cx.ring.tv.main;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.content.FileProvider;
import androidx.leanback.app.GuidedStepSupportFragment;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.HeaderItem;
import androidx.leanback.widget.ImageCardView;
import androidx.leanback.widget.ListRow;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowPresenter;
import androidx.tvprovider.media.tv.Channel;
import androidx.tvprovider.media.tv.ChannelLogoUtils;
import androidx.tvprovider.media.tv.PreviewProgram;
import androidx.tvprovider.media.tv.TvContractCompat;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import cx.ring.R;
import cx.ring.application.JamiApplication;
import cx.ring.contacts.AvatarFactory;
import cx.ring.fragments.ConversationFragment;
import cx.ring.model.Account;
import cx.ring.navigation.HomeNavigationViewModel;
import cx.ring.services.VCardServiceImpl;
import cx.ring.smartlist.SmartListViewModel;
import cx.ring.tv.about.AboutActivity;
import cx.ring.tv.account.TVAccountExport;
import cx.ring.tv.account.TVProfileEditingFragment;
import cx.ring.tv.account.TVSettingsActivity;
import cx.ring.tv.account.TVShareActivity;
import cx.ring.tv.call.TVCallActivity;
import cx.ring.tv.cards.Card;
import cx.ring.tv.cards.CardListRow;
import cx.ring.tv.cards.CardPresenterSelector;
import cx.ring.tv.cards.CardRow;
import cx.ring.tv.cards.ShadowRowPresenterSelector;
import cx.ring.tv.cards.contacts.ContactCard;
import cx.ring.tv.cards.iconcards.IconCard;
import cx.ring.tv.cards.iconcards.IconCardHelper;
import cx.ring.tv.contact.TVContactActivity;
import cx.ring.tv.search.SearchActivity;
import cx.ring.tv.views.CustomTitleView;
import cx.ring.utils.AndroidFileUtils;
import cx.ring.utils.BitmapUtils;
import cx.ring.utils.ContentUriHandler;
import cx.ring.utils.ConversationPath;
import cx.ring.utils.QRCodeUtils;
import cx.ring.views.AvatarDrawable;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
public class MainFragment extends BaseBrowseFragment<MainPresenter> implements MainView {
private static final String TAG = MainFragment.class.getSimpleName();
// Sections headers ids
private static final long HEADER_CONTACTS = 0;
private static final long HEADER_MISC = 1;
private static final int TRUST_REQUEST_ROW_POSITION = 1;
private static final int QR_ITEM_POSITION = 2;
private static final String PREFERENCES_CHANNELS = "channels";
private static final String KEY_CHANNEL_CONVERSATIONS = "conversations";
private static final Uri HOME_URI = new Uri.Builder()
.scheme(ContentUriHandler.SCHEME_TV)
.authority(ContentUriHandler.AUTHORITY)
.appendPath(ContentUriHandler.PATH_TV_HOME)
.build();
private SpinnerFragment mSpinnerFragment;
private ArrayObjectAdapter mRowsAdapter;
private ArrayObjectAdapter cardRowAdapter;
private ArrayObjectAdapter contactRequestRowAdapter;
private CustomTitleView titleView;
private CardListRow requestsRow;
private CardPresenterSelector selector;
private IconCard qrCard = null;
private ListRow myAccountRow;
private final CompositeDisposable mDisposable = new CompositeDisposable();
private final CompositeDisposable mHomeChannelDisposable = new CompositeDisposable();
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
((JamiApplication) getActivity().getApplication()).getInjectionComponent().inject(this);
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
titleView = view.findViewById(R.id.browse_title_group);
super.onViewCreated(view, savedInstanceState);
setupUIElements(requireActivity());
}
@Override
public void onDestroyView() {
super.onDestroyView();
mDisposable.clear();
}
private void setupUIElements(@NonNull Activity activity) {
selector = new CardPresenterSelector(activity);
// over title
setHeadersState(HEADERS_ENABLED);
setHeadersTransitionOnBackEnabled(true);
// set fastLane (or headers) background color
setBrandColor(getResources().getColor(R.color.color_primary_dark));
// set search icon color
setSearchAffordanceColor(getResources().getColor(R.color.color_primary_light));
mRowsAdapter = new ArrayObjectAdapter(new ShadowRowPresenterSelector());
/* Contact Presenter */
CardRow contactRow = new CardRow(
CardRow.TYPE_DEFAULT,
true,
getString(R.string.tv_contact_row_header),
new ArrayList<>());
HeaderItem cardPresenterHeader = new HeaderItem(HEADER_CONTACTS, getString(R.string.tv_contact_row_header));
cardRowAdapter = new ArrayObjectAdapter(selector);
CardListRow contactListRow = new CardListRow(cardPresenterHeader, cardRowAdapter, contactRow);
/* CardPresenter */
mRowsAdapter.add(contactListRow);
myAccountRow = createMyAccountRow(activity);
mRowsAdapter.add(myAccountRow);
mRowsAdapter.add(createAboutCardRow(activity));
setAdapter(mRowsAdapter);
// listeners
setOnSearchClickedListener(view -> startActivity(new Intent(getActivity(), SearchActivity.class)));
setOnItemViewClickedListener(new ItemViewClickedListener());
}
private ListRow createRow(String titleSection, List<Card> cards, boolean shadow) {
CardRow row = new CardRow(
CardRow.TYPE_DEFAULT,
shadow,
titleSection,
cards);
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(selector);
for (Card card : cards) {
listRowAdapter.add(card);
}
return new CardListRow(new HeaderItem(HEADER_MISC, titleSection), listRowAdapter, row);
}
private ListRow createMyAccountRow(@NonNull Context context) {
qrCard = IconCardHelper.getAccountShareCard(context, null);
List<Card> cards = new ArrayList<>(4);
cards.add(IconCardHelper.getAccountAddDeviceCard(context));
cards.add(IconCardHelper.getAccountManagementCard(context));
cards.add(qrCard);
cards.add(IconCardHelper.getAccountSettingsCard(context));
return createRow(getString(R.string.ring_account), cards, false);
}
private CardListRow createContactRequestRow() {
CardRow contactRequestRow = new CardRow(
CardRow.TYPE_DEFAULT,
true,
getString(R.string.menu_item_contact_request),
new ArrayList<ContactCard>());
contactRequestRowAdapter = new ArrayObjectAdapter(selector);
return new CardListRow(new HeaderItem(HEADER_MISC, getString(R.string.menu_item_contact_request)),
contactRequestRowAdapter,
contactRequestRow);
}
private Row createAboutCardRow(@NonNull Context context) {
List<Card> cards = new ArrayList<>(3);
cards.add(IconCardHelper.getVersionCard(context));
cards.add(IconCardHelper.getLicencesCard(context));
cards.add(IconCardHelper.getContributorCard(context));
return createRow(getString(R.string.menu_item_about), cards, false);
}
@Override
public void showLoading(final boolean show) {
if (show) {
mSpinnerFragment = new SpinnerFragment();
getParentFragmentManager().beginTransaction().replace(R.id.main_browse_fragment, mSpinnerFragment).commitAllowingStateLoss();
} else {
getParentFragmentManager().beginTransaction().remove(mSpinnerFragment).commitAllowingStateLoss();
}
}
@Override
public void refreshContact(final int index, final SmartListViewModel contact) {
ContactCard contactCard = (ContactCard) cardRowAdapter.get(index);
contactCard.setModel(contact);
cardRowAdapter.replace(index, contactCard);
}
@Override
public void showContacts(final List<SmartListViewModel> contacts) {
List<ContactCard> cards = new ArrayList<>(contacts.size());
for (SmartListViewModel contact : contacts)
cards.add(new ContactCard(contact));
cardRowAdapter.setItems(cards, null);
buildHomeChannel(requireContext().getApplicationContext(), contacts);
}
private static long createHomeChannel(Context context) {
Channel channel = new Channel.Builder()
.setType(TvContractCompat.Channels.TYPE_PREVIEW)
.setDisplayName(context.getString(R.string.navigation_item_conversation))
.setAppLinkIntentUri(HOME_URI)
.build();
ContentResolver cr = context.getContentResolver();
SharedPreferences sharedPref = context.getSharedPreferences(PREFERENCES_CHANNELS, Context.MODE_PRIVATE);
long channelId = sharedPref.getLong(KEY_CHANNEL_CONVERSATIONS, -1);
if (channelId == -1) {
Uri channelUri = cr.insert(TvContractCompat.Channels.CONTENT_URI, channel.toContentValues());
channelId = ContentUris.parseId(channelUri);
sharedPref.edit().putLong(KEY_CHANNEL_CONVERSATIONS, channelId).apply();
int targetSize = (int) (AvatarFactory.SIZE_NOTIF * context.getResources().getDisplayMetrics().density);
int targetPaddingSize = (int) (AvatarFactory.SIZE_PADDING * context.getResources().getDisplayMetrics().density);
ChannelLogoUtils.storeChannelLogo(context, channelId, BitmapUtils.drawableToBitmap(context.getDrawable(R.drawable.ic_jami_48), targetSize, targetPaddingSize));
TvContractCompat.requestChannelBrowsable(context, channelId);
} else {
cr.update(TvContractCompat.buildChannelUri(channelId), channel.toContentValues(), null, null);
}
return channelId;
}
private static Single<PreviewProgram> buildProgram(Context context, SmartListViewModel vm, String launcherName, long channelId) {
return new AvatarDrawable.Builder()
.withContact(vm.getContact())
.withPresence(false)
//.withPresence(vm.getContact().size() == 1)
//.withOnlineState(vm.getContact().size() == 1 && vm.getContact().get(0).isOnline())
.buildAsync(context)
.map(avatar -> {
File file = AndroidFileUtils.createImageFile(context);
Bitmap bitmapAvatar = BitmapUtils.drawableToBitmap(avatar, 256);
try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
bitmapAvatar.compress(Bitmap.CompressFormat.PNG, 100, os);
}
bitmapAvatar.recycle();
Uri uri = FileProvider.getUriForFile(context, ContentUriHandler.AUTHORITY_FILES, file);
// Grant permission to launcher
if (launcherName != null)
context.grantUriPermission(launcherName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
PreviewProgram.Builder contactBuilder = new PreviewProgram.Builder()
.setChannelId(channelId)
.setType(TvContractCompat.PreviewPrograms.TYPE_CLIP)
.setTitle(vm.getContactName())
.setAuthor(vm.getContact().getRingUsername())
.setPosterArtAspectRatio(TvContractCompat.PreviewPrograms.ASPECT_RATIO_1_1)
.setPosterArtUri(uri)
.setIntentUri(new Uri.Builder()
.scheme(ContentUriHandler.SCHEME_TV)
.authority(ContentUriHandler.AUTHORITY)
.appendPath(ContentUriHandler.PATH_TV_CONVERSATION)
.appendPath(vm.getAccountId())
.appendPath(vm.getUri().getUri())
.build())
.setInternalProviderId(vm.getUuid());
return contactBuilder.build();
});
}
private void buildHomeChannel(Context context, List<SmartListViewModel> contacts) {
if (contacts.isEmpty())
return;
// Get launcher package name
ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(
new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME), PackageManager.MATCH_DEFAULT_ONLY);
String launcherName = resolveInfo == null ? null : resolveInfo.activityInfo.packageName;
ContentResolver cr = context.getContentResolver();
mHomeChannelDisposable.clear();
mHomeChannelDisposable.add(Single.fromCallable(() -> createHomeChannel(context))
.doOnSuccess(channelId -> cr.delete(TvContractCompat.buildPreviewProgramsUriForChannel(channelId), null, null))
.flatMapObservable(channelId -> Observable.fromIterable(contacts)
.concatMapEager(contact -> buildProgram(context, contact, launcherName, channelId)
.toObservable()
.subscribeOn(Schedulers.io()), 8, 1))
.subscribeOn(Schedulers.io())
.subscribe(program -> cr.insert(TvContractCompat.PreviewPrograms.CONTENT_URI, program.toContentValues()),
e -> Log.w(TAG, "Error updating home channel", e)));
}
@Override
public void showContactRequests(final List<SmartListViewModel> contacts) {
CardListRow row = (CardListRow) mRowsAdapter.get(TRUST_REQUEST_ROW_POSITION);
boolean isRowDisplayed = row != null && row == requestsRow;
List<ContactCard> cards = new ArrayList<>(contacts.size());
for (SmartListViewModel contact : contacts)
cards.add(new ContactCard(contact));
if (isRowDisplayed && contacts.isEmpty()) {
mRowsAdapter.removeItems(TRUST_REQUEST_ROW_POSITION, 1);
} else if (!contacts.isEmpty()) {
if (requestsRow == null)
requestsRow = createContactRequestRow();
contactRequestRowAdapter.setItems(cards, null);
if (!isRowDisplayed)
mRowsAdapter.add(TRUST_REQUEST_ROW_POSITION, requestsRow);
}
}
@Override
public void callContact(String accountID, String number) {
Intent intent = new Intent(getActivity(), TVCallActivity.class);
intent.putExtra(ConversationFragment.KEY_ACCOUNT_ID, accountID);
intent.putExtra(ConversationFragment.KEY_CONTACT_RING_ID, number);
startActivity(intent, null);
}
static private BitmapDrawable prepareAccountQr(Context context, String accountId) {
Log.w(TAG, "prepareAccountQr " + accountId);
if (TextUtils.isEmpty(accountId))
return null;
QRCodeUtils.QRCodeData qrCodeData = QRCodeUtils.encodeStringAsQRCodeData(accountId, 0X00000000, 0xFFFFFFFF);
Bitmap bitmap = Bitmap.createBitmap(qrCodeData.getWidth(), qrCodeData.getHeight(), Bitmap.Config.ARGB_8888);
bitmap.setPixels(qrCodeData.getData(), 0, qrCodeData.getWidth(), 0, 0, qrCodeData.getWidth(), qrCodeData.getHeight());
return new BitmapDrawable(context.getResources(), bitmap);
}
@Override
public void displayAccountInfos(final HomeNavigationViewModel viewModel) {
Account account = viewModel.getAccount();
if (account != null)
updateModel(account);
}
@Override
public void updateModel(Account account) {
Context context = requireContext();
String address = account.getDisplayUsername();
mDisposable.clear();
mDisposable.add(VCardServiceImpl
.loadProfile(account)
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess(profile -> {
if (profile.first != null && !profile.first.isEmpty()) {
titleView.setAlias(profile.first);
if (address != null) {
setTitle(address);
} else {
setTitle("");
}
} else {
titleView.setAlias(address);
}
})
.flatMap(p -> AvatarDrawable.load(context, account))
.subscribe(a -> {
titleView.getLogoView().setVisibility(View.VISIBLE);
titleView.getLogoView().setImageDrawable(a);
}));
qrCard.setDrawable(prepareAccountQr(context, account.getUri()));
myAccountRow.getAdapter().notifyItemRangeChanged(QR_ITEM_POSITION, 1);
}
@Override
public void showExportDialog(String pAccountID) {
GuidedStepSupportFragment wizard = TVAccountExport.createInstance(pAccountID);
GuidedStepSupportFragment.add(getParentFragmentManager(), wizard, R.id.main_browse_fragment);
}
@Override
public void showProfileEditing() {
GuidedStepSupportFragment.add(getParentFragmentManager(), new TVProfileEditingFragment(), R.id.main_browse_fragment);
}
@Override
public void showAccountShare() {
Intent intent = new Intent(getActivity(), TVShareActivity.class);
startActivity(intent);
}
@Override
public void showLicence(int aboutType) {
Intent intent = new Intent(getActivity(), AboutActivity.class);
intent.putExtra("abouttype", aboutType);
startActivity(intent);
}
@Override
public void showSettings() {
startActivity(new Intent(getActivity(), TVSettingsActivity.class));
}
private final class ItemViewClickedListener implements OnItemViewClickedListener {
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
if (item instanceof ContactCard) {
SmartListViewModel model = ((ContactCard) item).getModel();
if (row == requestsRow) {
Intent intent = new Intent(Intent.ACTION_VIEW, null, requireContext(), TVContactActivity.class)
.setDataAndType(ConversationPath.toUri(model.getAccountId(), model.getUri()), TVContactActivity.TYPE_CONTACT_REQUEST_INCOMING);
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(),
((ImageCardView) itemViewHolder.view).getMainImageView(),
TVContactActivity.SHARED_ELEMENT_NAME).toBundle();
startActivity(intent, bundle);
} else {
Intent intent = new Intent(Intent.ACTION_VIEW, ConversationPath.toUri(model.getAccountId(), model.getUri()), getActivity(), TVContactActivity.class);
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(),
((ImageCardView) itemViewHolder.view).getMainImageView(),
TVContactActivity.SHARED_ELEMENT_NAME).toBundle();
startActivity(intent, bundle);
}
} else if (item instanceof IconCard) {
IconCard card = (IconCard) item;
switch (card.getType()) {
case ABOUT_CONTRIBUTOR:
case ABOUT_LICENCES:
presenter.onLicenceClicked(card.getType().ordinal());
break;
case ACCOUNT_ADD_DEVICE:
presenter.onExportClicked();
break;
case ACCOUNT_EDIT_PROFILE:
presenter.onEditProfileClicked();
break;
case ACCOUNT_SHARE_ACCOUNT:
ImageView view = ((ImageCardView) itemViewHolder.view).getMainImageView();
Intent intent = new Intent(getActivity(), TVShareActivity.class);
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, TVShareActivity.SHARED_ELEMENT_NAME).toBundle();
requireActivity().startActivity(intent, bundle);
break;
case ACCOUNT_SETTINGS:
presenter.onSettingsClicked();
break;
default:
break;
}
}
}
}
}