WelcomeActivity.java

  1package eu.siacs.conversations.ui;
  2
  3import android.Manifest;
  4import android.content.ActivityNotFoundException;
  5import android.content.Intent;
  6import android.content.pm.ActivityInfo;
  7import android.net.Uri;
  8import android.os.Bundle;
  9import android.security.KeyChain;
 10import android.security.KeyChainAliasCallback;
 11import android.util.Log;
 12import android.view.Menu;
 13import android.view.MenuItem;
 14import android.view.View;
 15import android.view.ViewGroup;
 16import android.widget.Toast;
 17
 18import androidx.annotation.NonNull;
 19import androidx.appcompat.app.AppCompatActivity;
 20import androidx.databinding.DataBindingUtil;
 21import androidx.viewpager.widget.PagerAdapter;
 22import androidx.viewpager.widget.ViewPager;
 23
 24import java.security.SecureRandom;
 25import java.util.Arrays;
 26import java.util.List;
 27import java.util.HashSet;
 28import java.util.UUID;
 29
 30import eu.siacs.conversations.Config;
 31import eu.siacs.conversations.R;
 32import eu.siacs.conversations.databinding.ActivityWelcomeBinding;
 33import eu.siacs.conversations.entities.Account;
 34import eu.siacs.conversations.services.XmppConnectionService;
 35import eu.siacs.conversations.utils.CryptoHelper;
 36import eu.siacs.conversations.utils.Compatibility;
 37import eu.siacs.conversations.utils.InstallReferrerUtils;
 38import eu.siacs.conversations.utils.SignupUtils;
 39import eu.siacs.conversations.utils.XmppUri;
 40import eu.siacs.conversations.xmpp.Jid;
 41
 42import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
 43import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
 44
 45public class WelcomeActivity extends XmppActivity implements XmppConnectionService.OnAccountCreated, XmppConnectionService.OnAccountUpdate, KeyChainAliasCallback {
 46
 47    private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
 48
 49    private XmppUri inviteUri;
 50    private Account onboardingAccount = null;
 51
 52    public static void launch(AppCompatActivity activity) {
 53        Intent intent = new Intent(activity, WelcomeActivity.class);
 54        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
 55        activity.startActivity(intent);
 56        activity.overridePendingTransition(0, 0);
 57    }
 58
 59    public void onInstallReferrerDiscovered(final Uri referrer) {
 60        Log.d(Config.LOGTAG, "welcome activity: on install referrer discovered " + referrer);
 61        if ("xmpp".equalsIgnoreCase(referrer.getScheme())) {
 62            final XmppUri xmppUri = new XmppUri(referrer);
 63            runOnUiThread(() -> processXmppUri(xmppUri));
 64        } else {
 65            Log.i(Config.LOGTAG, "install referrer was not an XMPP uri");
 66        }
 67    }
 68
 69    private void processXmppUri(final XmppUri xmppUri) {
 70        if (!xmppUri.isValidJid()) {
 71            return;
 72        }
 73        final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH);
 74        final Jid jid = xmppUri.getJid();
 75        final Intent intent;
 76        if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
 77            intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth);
 78        } else if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) {
 79            intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth);
 80            intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
 81        } else {
 82            intent = null;
 83        }
 84        if (intent != null) {
 85            startActivity(intent);
 86            finish();
 87            return;
 88        }
 89        this.inviteUri = xmppUri;
 90    }
 91
 92    @Override
 93    protected synchronized void refreshUiReal() {
 94        if (onboardingAccount == null) return;
 95        if (onboardingAccount.getStatus() != Account.State.ONLINE) return;
 96
 97        Intent intent = new Intent(this, StartConversationActivity.class);
 98        intent.putExtra("init", true);
 99        intent.putExtra(EXTRA_ACCOUNT, onboardingAccount.getJid().asBareJid().toEscapedString());
100        onboardingAccount = null;
101        startActivity(intent);
102        finish();
103    }
104
105    @Override
106    public void onAccountUpdate() {
107        refreshUi();
108    }
109
110    @Override
111    void onBackendConnected() {
112
113    }
114
115    @Override
116    public void onStart() {
117        super.onStart();
118        final int theme = findTheme();
119        if (this.mTheme != theme) {
120            recreate();
121        }
122        new InstallReferrerUtils(this);
123    }
124
125    @Override
126    public void onStop() {
127        super.onStop();
128    }
129
130    @Override
131    public void onNewIntent(Intent intent) {
132        if (intent != null) {
133            setIntent(intent);
134        }
135    }
136
137    @Override
138    protected void onCreate(final Bundle savedInstanceState) {
139        if (getResources().getBoolean(R.bool.portrait_only)) {
140            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
141        }
142        super.onCreate(savedInstanceState);
143        getPreferences().edit().putStringSet("pstn_gateways", new HashSet<>()).apply();
144        ActivityWelcomeBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_welcome);
145        binding.slideshowPager.setAdapter(new WelcomePagerAdapter(binding.slideshowPager));
146        binding.dotsIndicator.setViewPager(binding.slideshowPager);
147        binding.slideshowPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
148            public void onPageScrollStateChanged(int state) { }
149            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }
150
151            public void onPageSelected(int position) {
152                binding.buttonNext.setVisibility(position > 1 ? View.GONE : View.VISIBLE);
153                binding.buttonPrivacy.setVisibility(position < 2 ? View.GONE : View.VISIBLE);
154            }
155        });
156        binding.buttonNext.setOnClickListener((v) ->
157            binding.slideshowPager.setCurrentItem(binding.slideshowPager.getCurrentItem() + 1)
158        );
159        binding.buttonPrivacy.setOnClickListener((v) ->
160            startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://cheogram.com/android-privacy.html")))
161        );
162        setSupportActionBar(binding.toolbar);
163        configureActionBar(getSupportActionBar(), false);
164        binding.registerNewAccount.setOnClickListener(v -> {
165            if (hasInviteUri()) {
166                final Intent intent = new Intent(this, MagicCreateActivity.class);
167                addInviteUri(intent);
168                startActivity(intent);
169            } else {
170                binding.registerNewAccount.setText("Working...");
171                binding.registerNewAccount.setEnabled(false);
172                onboardingAccount = new Account(Jid.ofLocalAndDomain(UUID.randomUUID().toString(), Config.ONBOARDING_DOMAIN.toEscapedString()), CryptoHelper.createPassword(new SecureRandom()));
173                onboardingAccount.setOption(Account.OPTION_REGISTER, true);
174                onboardingAccount.setOption(Account.OPTION_FIXED_USERNAME, true);
175                xmppConnectionService.createAccount(onboardingAccount);
176            }
177        });
178        binding.useExisting.setOnClickListener(v -> {
179            final List<Account> accounts = xmppConnectionService.getAccounts();
180            Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class);
181            intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, false);
182            if (accounts.size() == 1) {
183                intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString());
184                intent.putExtra("init", true);
185            } else if (accounts.size() >= 1) {
186                intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class);
187            }
188            addInviteUri(intent);
189            startActivity(intent);
190        });
191        binding.useSnikket.setOnClickListener(v -> {
192            final List<Account> accounts = xmppConnectionService.getAccounts();
193            Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class);
194            intent.putExtra(EditAccountActivity.EXTRA_FORCE_REGISTER, false);
195            intent.putExtra("snikket", true);
196            if (accounts.size() == 1) {
197                intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString());
198                intent.putExtra("init", true);
199            } else if (accounts.size() >= 1) {
200                intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class);
201            }
202            addInviteUri(intent);
203            startActivity(intent);
204        });
205
206        binding.useBackup.setOnClickListener(v -> {
207            if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
208                startActivity(new Intent(this, ImportBackupActivity.class));
209            }
210        });
211    }
212
213    @Override
214    public boolean onCreateOptionsMenu(Menu menu) {
215        getMenuInflater().inflate(R.menu.welcome_menu, menu);
216        final MenuItem scan = menu.findItem(R.id.action_scan_qr_code);
217        scan.setVisible(Compatibility.hasFeatureCamera(this));
218        return super.onCreateOptionsMenu(menu);
219    }
220
221
222
223    @Override
224    public boolean onOptionsItemSelected(MenuItem item) {
225        switch (item.getItemId()) {
226            case R.id.action_import_backup:
227                if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) {
228                    startActivity(new Intent(this, ImportBackupActivity.class));
229                }
230                break;
231            case R.id.action_scan_qr_code:
232                UriHandlerActivity.scan(this, true);
233                break;
234            case R.id.action_add_account_with_cert:
235                addAccountFromKey();
236                break;
237        }
238        return super.onOptionsItemSelected(item);
239    }
240
241    private void addAccountFromKey() {
242        try {
243            KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null);
244        } catch (ActivityNotFoundException e) {
245            Toast.makeText(this, R.string.device_does_not_support_certificates, Toast.LENGTH_LONG).show();
246        }
247    }
248
249    @Override
250    public void alias(final String alias) {
251        if (alias != null) {
252            xmppConnectionService.createAccountFromKey(alias, this);
253        }
254    }
255
256    @Override
257    public void onAccountCreated(final Account account) {
258        final Intent intent = new Intent(this, EditAccountActivity.class);
259        intent.putExtra("jid", account.getJid().asBareJid().toEscapedString());
260        intent.putExtra("init", true);
261        addInviteUri(intent);
262        startActivity(intent);
263    }
264
265    @Override
266    public void informUser(final int r) {
267        runOnUiThread(() -> Toast.makeText(this, r, Toast.LENGTH_LONG).show());
268    }
269
270    @Override
271    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
272        UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
273        if (grantResults.length > 0) {
274            if (allGranted(grantResults)) {
275                switch (requestCode) {
276                    case REQUEST_IMPORT_BACKUP:
277                        startActivity(new Intent(this, ImportBackupActivity.class));
278                        break;
279                }
280            } else if (Arrays.asList(permissions).contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
281                Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show();
282            }
283        }
284        if (writeGranted(grantResults, permissions)) {
285            if (xmppConnectionService != null) {
286                xmppConnectionService.restartFileObserver();
287            }
288        }
289    }
290
291    protected boolean hasInviteUri() {
292        final Intent from = getIntent();
293        if (from != null && from.hasExtra(StartConversationActivity.EXTRA_INVITE_URI)) return true;
294        return this.inviteUri != null;
295    }
296
297    public void addInviteUri(Intent to) {
298        final Intent from = getIntent();
299        if (from != null && from.hasExtra(StartConversationActivity.EXTRA_INVITE_URI)) {
300            final String invite = from.getStringExtra(StartConversationActivity.EXTRA_INVITE_URI);
301            to.putExtra(StartConversationActivity.EXTRA_INVITE_URI, invite);
302        } else if (this.inviteUri != null) {
303            Log.d(Config.LOGTAG, "injecting referrer uri into on-boarding flow");
304            to.putExtra(StartConversationActivity.EXTRA_INVITE_URI, this.inviteUri.toString());
305        }
306    }
307
308    class WelcomePagerAdapter extends PagerAdapter {
309        protected View[] pages;
310
311        public WelcomePagerAdapter(ViewPager p) {
312            super();
313            pages = new View[]{ p.getChildAt(0), p.getChildAt(1), p.getChildAt(2) };
314            for (View v : pages) {
315                p.removeView(v);
316            }
317        }
318
319        @Override
320        public int getCount() {
321            return 3;
322        }
323
324        @NonNull
325        @Override
326        public Object instantiateItem(@NonNull ViewGroup container, int position) {
327            container.addView(pages[position]);
328            return pages[position];
329        }
330
331        @Override
332        public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
333            return view == o;
334        }
335
336        @Override
337        public void destroyItem(@NonNull ViewGroup container, int position, Object o) {
338            container.removeView(pages[position]);
339        }
340    }
341}