Add support for selecting account when sharing from outside apps (#1011)

* Add direct-share support (API 23+)

* Add account selection dialog for non-direct sharing
This commit is contained in:
Levi Bard 2019-02-06 10:23:02 +01:00 committed by Konrad Pozniak
parent e98c7ac435
commit d5173c2268
10 changed files with 248 additions and 33 deletions

View File

@ -48,11 +48,6 @@
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|keyboardHidden">
</activity>
<activity
android:name=".ComposeActivity"
android:theme="@style/TuskyDialogActivityTheme"
android:windowSoftInputMode="stateVisible|adjustResize">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
@ -78,6 +73,15 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<meta-data
android:name="android.service.chooser.chooser_target_service"
android:value="com.keylesspalace.tusky.service.AccountChooserService"
/>
</activity>
<activity
android:name=".ComposeActivity"
android:theme="@style/TuskyDialogActivityTheme"
android:windowSoftInputMode="stateVisible|adjustResize">
</activity>
<activity
android:name=".ViewThreadActivity"
@ -129,6 +133,16 @@
</intent-filter>
</service>
<service android:name=".service.SendTootService" />
<service
tools:targetApi="23"
android:name="com.keylesspalace.tusky.service.AccountChooserService"
android:label="@string/app_name"
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE"
>
<intent-filter>
<action android:name="android.service.chooser.ChooserTargetService" />
</intent-filter>
</service>
<provider
android:name="androidx.core.content.FileProvider"

View File

@ -16,6 +16,7 @@
package com.keylesspalace.tusky;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
@ -31,9 +32,11 @@ import android.view.Menu;
import android.view.View;
import com.google.android.material.snackbar.Snackbar;
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.interfaces.AccountSelectionListener;
import com.keylesspalace.tusky.util.ThemeUtils;
import java.util.ArrayList;
@ -178,4 +181,36 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
}
super.onDestroy();
}
public void showAccountChooserDialog(CharSequence dialogTitle, boolean showActiveAccount, AccountSelectionListener listener) {
List<AccountEntity> accounts = accountManager.getAllAccountsOrderedByActive();
AccountEntity activeAccount = accountManager.getActiveAccount();
switch(accounts.size()) {
case 1:
listener.onAccountSelected(activeAccount);
return;
case 2:
if (!showActiveAccount) {
for (AccountEntity account : accounts) {
if (activeAccount != account) {
listener.onAccountSelected(account);
return;
}
}
}
break;
}
if (!showActiveAccount && activeAccount != null) {
accounts.remove(activeAccount);
}
AccountSelectionAdapter adapter = new AccountSelectionAdapter(this);
adapter.addAll(accounts);
new AlertDialog.Builder(this)
.setTitle(dialogTitle)
.setAdapter(adapter, (dialogInterface, index) -> listener.onAccountSelected(accounts.get(index)))
.show();
}
}

View File

@ -1615,6 +1615,11 @@ public final class ComposeActivity
return maximumTootCharacters;
}
static boolean canHandleMimeType(@Nullable String mimeType) {
return (mimeType != null &&
(mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.equals("text/plain")));
}
public static final class QueuedMedia {
Type type;
ProgressImageView preview;

View File

@ -108,6 +108,14 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
private Drawer drawer;
private ViewPager viewPager;
private void forwardShare(Intent intent) {
Intent composeIntent = new Intent(this, ComposeActivity.class);
composeIntent.setAction(intent.getAction());
composeIntent.setType(intent.getType());
composeIntent.putExtras(intent);
startActivity(composeIntent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -117,18 +125,40 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
if (intent != null) {
long accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1);
boolean accountRequested = (accountId != -1);
if (accountId != -1) {
// user clicked a notification, show notification tab and switch user if necessary
tabPosition = 1;
if (accountRequested) {
AccountEntity account = accountManager.getActiveAccount();
if (account == null || accountId != account.getId()) {
accountManager.setActiveAccount(accountId);
}
}
}
if (ComposeActivity.canHandleMimeType(intent.getType())) {
// Sharing to Tusky from an external app
if (accountRequested) {
// The correct account is already active
forwardShare(intent);
} else {
// No account was provided, show the chooser
showAccountChooserDialog(getString(R.string.action_share_as), true, account -> {
long requestedId = account.getId();
AccountEntity activeAccount = accountManager.getActiveAccount();
if (activeAccount != null && requestedId == activeAccount.getId()) {
// The correct account is already active
forwardShare(intent);
} else {
// A different account was requested, restart the activity
intent.putExtra(NotificationHelper.ACCOUNT_ID, requestedId);
changeAccount(requestedId, intent);
}
});
}
} else if (accountRequested) {
// user clicked a notification, show notification tab and switch user if necessary
tabPosition = 1;
}
}
setContentView(R.layout.activity_main);
composeButton = findViewById(R.id.floating_btn);
@ -420,17 +450,22 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
return true;
}
//change Account
changeAccount(profile.getIdentifier());
changeAccount(profile.getIdentifier(), null);
return false;
}
private void changeAccount(long newSelectedId) {
private void changeAccount(long newSelectedId, @Nullable Intent forward) {
cacheUpdater.stop();
accountManager.setActiveAccount(newSelectedId);
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
if (forward != null) {
intent.setType(forward.getType());
intent.setAction(forward.getAction());
intent.putExtras(forward);
}
startActivity(intent);
finishWithoutSlideOutAnimation();

View File

@ -0,0 +1,58 @@
/* Copyright 2019 Levi Bard
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter
import android.content.Context
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.util.CustomEmojiHelper
import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.item_autocomplete_account.view.*
class AccountSelectionAdapter(context: Context): ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var view = convertView
if (convertView == null) {
val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
view = layoutInflater.inflate(R.layout.item_autocomplete_account, parent, false)
}
view!!
val account = getItem(position)
if (account != null) {
val username = view.username
val displayName = view.display_name
val avatar = view.avatar
username.text = account.fullName
displayName.text = CustomEmojiHelper.emojifyString(account.displayName, account.emojis, displayName)
if (!TextUtils.isEmpty(account.profilePictureUrl)) {
Picasso.with(context)
.load(account.profilePictureUrl)
.placeholder(R.drawable.avatar_default)
.into(avatar)
}
}
return view
}
}

View File

@ -15,6 +15,7 @@
package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.service.AccountChooserService
import com.keylesspalace.tusky.service.SendTootService
import dagger.Module
import dagger.android.ContributesAndroidInjector
@ -23,4 +24,6 @@ import dagger.android.ContributesAndroidInjector
abstract class ServicesModule {
@ContributesAndroidInjector
abstract fun contributesSendTootService(): SendTootService
@ContributesAndroidInjector
abstract fun contributesAccountChooserService(): AccountChooserService
}

View File

@ -337,26 +337,7 @@ public abstract class SFragment extends BaseFragment {
}
private void showOpenAsDialog(String statusUrl, CharSequence dialogTitle) {
List<AccountEntity> accounts = accountManager.getAllAccountsOrderedByActive();
AccountEntity activeAccount = accountManager.getActiveAccount();
if (accounts.size() == 2) {
for (AccountEntity account : accounts) {
if (activeAccount != account) {
openAsAccount(statusUrl, account);
break;
}
}
} else {
accounts.remove(activeAccount);
CharSequence[] accountNames = new CharSequence[accounts.size()];
for (int i = 0; i < accounts.size(); ++i) {
accountNames[i] = accounts.get(i).getFullName();
}
new AlertDialog.Builder(getActivity())
.setTitle(dialogTitle)
.setItems(accountNames, (dialogInterface, index) -> openAsAccount(statusUrl, accounts.get(index)))
.show();
}
BaseActivity activity = (BaseActivity)getActivity();
activity.showAccountChooserDialog(dialogTitle, false, account -> openAsAccount(statusUrl, account));
}
}

View File

@ -0,0 +1,22 @@
/* Copyright 2019 Levi Bard
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.interfaces
import com.keylesspalace.tusky.db.AccountEntity
interface AccountSelectionListener {
fun onAccountSelected(account: AccountEntity)
}

View File

@ -0,0 +1,61 @@
/* Copyright 2019 Levi Bard
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.service
import android.annotation.TargetApi
import android.content.ComponentName
import android.content.IntentFilter
import android.graphics.drawable.Icon
import android.os.Bundle
import android.service.chooser.ChooserTarget
import android.service.chooser.ChooserTargetService
import android.text.TextUtils
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.NotificationHelper
import com.squareup.picasso.Picasso
import dagger.android.AndroidInjection
import javax.inject.Inject
@TargetApi(23)
class AccountChooserService : ChooserTargetService(), Injectable {
@Inject
lateinit var accountManager: AccountManager
override fun onCreate() {
super.onCreate()
AndroidInjection.inject(this)
}
override fun onGetChooserTargets(targetActivityName: ComponentName?, intentFilter: IntentFilter?): MutableList<ChooserTarget> {
val targets = mutableListOf<ChooserTarget>()
for (account in accountManager.getAllAccountsOrderedByActive()) {
val icon: Icon = if (TextUtils.isEmpty(account.profilePictureUrl)) {
Icon.createWithResource(applicationContext, R.drawable.avatar_default)
} else {
Icon.createWithBitmap(Picasso.with(this).load(account.profilePictureUrl)
.error(R.drawable.avatar_default)
.placeholder(R.drawable.avatar_default)
.get())
}
val bundle = Bundle()
bundle.putLong(NotificationHelper.ACCOUNT_ID, account.id)
targets.add(ChooserTarget(account.displayName, icon, 1.0f, targetActivityName, bundle))
}
return targets
}
}

View File

@ -116,6 +116,7 @@
<string name="action_copy_link">Copy the link</string>
<string name="action_open_as">Open as %s</string>
<string name="action_share_as">Share as …</string>
<string name="send_status_link_to">Share toot URL to…</string>
<string name="send_status_content_to">Share toot to…</string>