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:
parent
e98c7ac435
commit
d5173c2268
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue