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