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
10 changed files with 248 additions and 33 deletions
|
@ -48,11 +48,6 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|screenSize|keyboardHidden">
|
android:configChanges="orientation|screenSize|keyboardHidden">
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".ComposeActivity"
|
|
||||||
android:theme="@style/TuskyDialogActivityTheme"
|
|
||||||
android:windowSoftInputMode="stateVisible|adjustResize">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
@ -78,6 +73,15 @@
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="video/*" />
|
<data android:mimeType="video/*" />
|
||||||
</intent-filter>
|
</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>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ViewThreadActivity"
|
android:name=".ViewThreadActivity"
|
||||||
|
@ -129,6 +133,16 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service android:name=".service.SendTootService" />
|
<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
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
package com.keylesspalace.tusky;
|
package com.keylesspalace.tusky;
|
||||||
|
|
||||||
import android.app.ActivityManager;
|
import android.app.ActivityManager;
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
@ -31,9 +32,11 @@ import android.view.Menu;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
|
||||||
import com.keylesspalace.tusky.db.AccountEntity;
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
|
import com.keylesspalace.tusky.interfaces.AccountSelectionListener;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -178,4 +181,36 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
||||||
}
|
}
|
||||||
super.onDestroy();
|
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;
|
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 {
|
public static final class QueuedMedia {
|
||||||
Type type;
|
Type type;
|
||||||
ProgressImageView preview;
|
ProgressImageView preview;
|
||||||
|
|
|
@ -108,6 +108,14 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||||
private Drawer drawer;
|
private Drawer drawer;
|
||||||
private ViewPager viewPager;
|
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
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
@ -117,18 +125,40 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||||
|
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
long accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1);
|
long accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1);
|
||||||
|
boolean accountRequested = (accountId != -1);
|
||||||
|
|
||||||
if (accountId != -1) {
|
if (accountRequested) {
|
||||||
// user clicked a notification, show notification tab and switch user if necessary
|
|
||||||
tabPosition = 1;
|
|
||||||
AccountEntity account = accountManager.getActiveAccount();
|
AccountEntity account = accountManager.getActiveAccount();
|
||||||
|
|
||||||
if (account == null || accountId != account.getId()) {
|
if (account == null || accountId != account.getId()) {
|
||||||
accountManager.setActiveAccount(accountId);
|
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);
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
composeButton = findViewById(R.id.floating_btn);
|
composeButton = findViewById(R.id.floating_btn);
|
||||||
|
@ -420,17 +450,22 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
//change Account
|
//change Account
|
||||||
changeAccount(profile.getIdentifier());
|
changeAccount(profile.getIdentifier(), null);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void changeAccount(long newSelectedId) {
|
private void changeAccount(long newSelectedId, @Nullable Intent forward) {
|
||||||
cacheUpdater.stop();
|
cacheUpdater.stop();
|
||||||
accountManager.setActiveAccount(newSelectedId);
|
accountManager.setActiveAccount(newSelectedId);
|
||||||
|
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
Intent intent = new Intent(this, MainActivity.class);
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
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);
|
startActivity(intent);
|
||||||
finishWithoutSlideOutAnimation();
|
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
|
package com.keylesspalace.tusky.di
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.service.AccountChooserService
|
||||||
import com.keylesspalace.tusky.service.SendTootService
|
import com.keylesspalace.tusky.service.SendTootService
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
@ -23,4 +24,6 @@ import dagger.android.ContributesAndroidInjector
|
||||||
abstract class ServicesModule {
|
abstract class ServicesModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesSendTootService(): SendTootService
|
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) {
|
private void showOpenAsDialog(String statusUrl, CharSequence dialogTitle) {
|
||||||
List<AccountEntity> accounts = accountManager.getAllAccountsOrderedByActive();
|
BaseActivity activity = (BaseActivity)getActivity();
|
||||||
AccountEntity activeAccount = accountManager.getActiveAccount();
|
activity.showAccountChooserDialog(dialogTitle, false, account -> openAsAccount(statusUrl, account));
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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_copy_link">Copy the link</string>
|
||||||
<string name="action_open_as">Open as %s</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_link_to">Share toot URL to…</string>
|
||||||
<string name="send_status_content_to">Share toot to…</string>
|
<string name="send_status_content_to">Share toot to…</string>
|
||||||
|
|
Loading…
Add table
Reference in a new issue