cid#1607171 Data race condition
[LibreOffice.git] / android / source / src / java / org / libreoffice / ui / LibreOfficeUIActivity.java
blob3f93d815af272bd323ebb079be57db044ad3feb1
1 /* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 */
10 package org.libreoffice.ui;
12 import android.Manifest;
13 import android.content.ActivityNotFoundException;
14 import android.content.ComponentName;
15 import android.content.Context;
16 import android.content.Intent;
17 import android.content.SharedPreferences;
18 import android.content.pm.PackageManager;
19 import android.content.pm.ShortcutInfo;
20 import android.content.pm.ShortcutManager;
21 import android.graphics.drawable.Icon;
22 import android.net.Uri;
23 import android.os.Bundle;
24 import com.google.android.material.floatingactionbutton.FloatingActionButton;
25 import androidx.core.app.ActivityCompat;
26 import androidx.core.content.ContextCompat;
27 import androidx.core.view.ViewCompat;
28 import androidx.appcompat.app.ActionBar;
29 import androidx.appcompat.app.AppCompatActivity;
30 import androidx.recyclerview.widget.GridLayoutManager;
31 import androidx.recyclerview.widget.RecyclerView;
32 import androidx.preference.PreferenceManager;
33 import androidx.appcompat.widget.Toolbar;
34 import android.text.TextUtils;
35 import android.util.Log;
36 import android.view.Menu;
37 import android.view.MenuInflater;
38 import android.view.MenuItem;
39 import android.view.View;
40 import android.view.animation.Animation;
41 import android.view.animation.AnimationUtils;
42 import android.view.animation.OvershootInterpolator;
43 import android.widget.LinearLayout;
44 import android.widget.TextView;
46 import org.libreoffice.AboutDialogFragment;
47 import org.libreoffice.BuildConfig;
48 import org.libreoffice.LibreOfficeMainActivity;
49 import org.libreoffice.R;
50 import org.libreoffice.SettingsActivity;
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.List;
56 public class LibreOfficeUIActivity extends AppCompatActivity implements View.OnClickListener{
57 public enum DocumentType {
58 WRITER,
59 CALC,
60 IMPRESS,
61 DRAW,
62 INVALID
65 private static final String LOGTAG = LibreOfficeUIActivity.class.getSimpleName();
67 public static final String EXPLORER_PREFS_KEY = "EXPLORER_PREFS";
68 private static final String RECENT_DOCUMENTS_KEY = "RECENT_DOCUMENT_URIS";
69 // delimiter used for storing multiple URIs in a string
70 private static final String RECENT_DOCUMENTS_DELIMITER = " ";
72 public static final String NEW_DOC_TYPE_KEY = "NEW_DOC_TYPE_KEY";
73 public static final String NEW_WRITER_STRING_KEY = "private:factory/swriter";
74 public static final String NEW_IMPRESS_STRING_KEY = "private:factory/simpress";
75 public static final String NEW_CALC_STRING_KEY = "private:factory/scalc";
76 public static final String NEW_DRAW_STRING_KEY = "private:factory/sdraw";
78 // keep this in sync with 'AndroidManifext.xml'
79 private static final String[] SUPPORTED_MIME_TYPES = {
80 "application/vnd.oasis.opendocument.text",
81 "application/vnd.oasis.opendocument.graphics",
82 "application/vnd.oasis.opendocument.presentation",
83 "application/vnd.oasis.opendocument.spreadsheet",
84 "application/vnd.oasis.opendocument.text-flat-xml",
85 "application/vnd.oasis.opendocument.graphics-flat-xml",
86 "application/vnd.oasis.opendocument.presentation-flat-xml",
87 "application/vnd.oasis.opendocument.spreadsheet-flat-xml",
88 "application/vnd.oasis.opendocument.text-template",
89 "application/vnd.oasis.opendocument.spreadsheet-template",
90 "application/vnd.oasis.opendocument.graphics-template",
91 "application/vnd.oasis.opendocument.presentation-template",
92 "application/rtf",
93 "text/rtf",
94 "application/msword",
95 "application/vnd.ms-powerpoint",
96 "application/vnd.ms-excel",
97 "application/vnd.visio",
98 "application/vnd.visio2013",
99 "application/vnd.visio.xml",
100 "application/x-mspublisher",
101 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
102 "application/vnd.openxmlformats-officedocument.presentationml.presentation",
103 "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
104 "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
105 "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
106 "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
107 "application/vnd.openxmlformats-officedocument.presentationml.template",
108 "text/csv",
109 "text/comma-separated-values",
110 "application/vnd.ms-works",
111 "application/vnd.apple.keynote",
112 "application/x-abiword",
113 "application/x-pagemaker",
114 "image/x-emf",
115 "image/x-svm",
116 "image/x-wmf",
117 "image/svg+xml",
120 private static final int REQUEST_CODE_OPEN_FILECHOOSER = 12345;
122 private static final int PERMISSION_WRITE_EXTERNAL_STORAGE = 0;
124 private Animation fabOpenAnimation;
125 private Animation fabCloseAnimation;
126 private boolean isFabMenuOpen = false;
127 private FloatingActionButton editFAB;
128 private FloatingActionButton writerFAB;
129 private FloatingActionButton drawFAB;
130 private FloatingActionButton impressFAB;
131 private FloatingActionButton calcFAB;
132 private LinearLayout drawLayout;
133 private LinearLayout writerLayout;
134 private LinearLayout impressLayout;
135 private LinearLayout calcLayout;
137 @Override
138 public void onCreate(Bundle savedInstanceState) {
139 super.onCreate(savedInstanceState);
141 // init UI
142 createUI();
143 fabOpenAnimation = AnimationUtils.loadAnimation(this, R.anim.fab_open);
144 fabCloseAnimation = AnimationUtils.loadAnimation(this, R.anim.fab_close);
147 @Override
148 protected void onStart() {
149 super.onStart();
150 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
151 Log.i(LOGTAG, "no permission to read external storage - asking for permission");
152 ActivityCompat.requestPermissions(this,
153 new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
154 PERMISSION_WRITE_EXTERNAL_STORAGE);
158 public void createUI() {
159 setContentView(R.layout.activity_document_browser);
161 Toolbar toolbar = findViewById(R.id.toolbar);
162 setSupportActionBar(toolbar);
163 ActionBar actionBar = getSupportActionBar();
165 if (actionBar != null) {
166 actionBar.setIcon(R.mipmap.ic_launcher);
169 editFAB = findViewById(R.id.editFAB);
170 editFAB.setOnClickListener(this);
171 // allow creating new docs only when experimental editing is enabled
172 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
173 final boolean bEditingEnabled = BuildConfig.ALLOW_EDITING && preferences.getBoolean(LibreOfficeMainActivity.ENABLE_EXPERIMENTAL_PREFS_KEY, false);
174 editFAB.setVisibility(bEditingEnabled ? View.VISIBLE : View.INVISIBLE);
176 impressFAB = findViewById(R.id.newImpressFAB);
177 impressFAB.setOnClickListener(this);
178 writerFAB = findViewById(R.id.newWriterFAB);
179 writerFAB.setOnClickListener(this);
180 calcFAB = findViewById(R.id.newCalcFAB);
181 calcFAB.setOnClickListener(this);
182 drawFAB = findViewById(R.id.newDrawFAB);
183 drawFAB.setOnClickListener(this);
184 writerLayout = findViewById(R.id.writerLayout);
185 impressLayout = findViewById(R.id.impressLayout);
186 calcLayout = findViewById(R.id.calcLayout);
187 drawLayout = findViewById(R.id.drawLayout);
188 TextView openFileView = findViewById(R.id.open_file_button);
189 openFileView.setOnClickListener(this);
192 RecyclerView recentRecyclerView = findViewById(R.id.list_recent);
194 SharedPreferences prefs = getSharedPreferences(EXPLORER_PREFS_KEY, MODE_PRIVATE);
195 String recentPref = prefs.getString(RECENT_DOCUMENTS_KEY, "");
196 String[] recentFileStrings = recentPref.split(RECENT_DOCUMENTS_DELIMITER);
198 final List<RecentFile> recentFiles = new ArrayList<>();
199 for (String recentFileString : recentFileStrings) {
200 Uri uri = Uri.parse(recentFileString);
201 String filename = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), uri);
202 if (!filename.isEmpty()) {
203 recentFiles.add(new RecentFile(uri, filename));
207 recentRecyclerView.setLayoutManager(new GridLayoutManager(this, 2));
208 recentRecyclerView.setAdapter(new RecentFilesAdapter(this, recentFiles));
211 private void expandFabMenu() {
212 ViewCompat.animate(editFAB).rotation(45.0F).withLayer().setDuration(300).setInterpolator(new OvershootInterpolator(10.0F)).start();
213 drawLayout.startAnimation(fabOpenAnimation);
214 impressLayout.startAnimation(fabOpenAnimation);
215 writerLayout.startAnimation(fabOpenAnimation);
216 calcLayout.startAnimation(fabOpenAnimation);
217 writerFAB.setClickable(true);
218 impressFAB.setClickable(true);
219 drawFAB.setClickable(true);
220 calcFAB.setClickable(true);
221 isFabMenuOpen = true;
224 private void collapseFabMenu() {
225 ViewCompat.animate(editFAB).rotation(0.0F).withLayer().setDuration(300).setInterpolator(new OvershootInterpolator(10.0F)).start();
226 writerLayout.startAnimation(fabCloseAnimation);
227 impressLayout.startAnimation(fabCloseAnimation);
228 drawLayout.startAnimation(fabCloseAnimation);
229 calcLayout.startAnimation(fabCloseAnimation);
230 writerFAB.setClickable(false);
231 impressFAB.setClickable(false);
232 drawFAB.setClickable(false);
233 calcFAB.setClickable(false);
234 isFabMenuOpen = false;
237 @Override
238 public void onBackPressed() {
239 if (isFabMenuOpen) {
240 collapseFabMenu();
241 } else {
242 super.onBackPressed();
246 @Override
247 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
248 super.onActivityResult(requestCode, resultCode, data);
249 if (requestCode == REQUEST_CODE_OPEN_FILECHOOSER && resultCode == RESULT_OK) {
250 final Uri fileUri = data.getData();
251 openDocument(fileUri);
255 private void showSystemFilePickerAndOpenFile() {
256 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
257 intent.setType("*/*");
258 intent.putExtra(Intent.EXTRA_MIME_TYPES, SUPPORTED_MIME_TYPES);
260 try {
261 startActivityForResult(intent, REQUEST_CODE_OPEN_FILECHOOSER);
262 } catch (ActivityNotFoundException e) {
263 Log.w(LOGTAG, "No activity available that can handle the intent to open a document.");
267 public void openDocument(final Uri documentUri) {
268 // "forward" to LibreOfficeMainActivity to open the file
269 Intent intent = new Intent(Intent.ACTION_VIEW, documentUri);
270 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
272 addDocumentToRecents(documentUri);
274 String packageName = getApplicationContext().getPackageName();
275 ComponentName componentName = new ComponentName(packageName,
276 LibreOfficeMainActivity.class.getName());
277 intent.setComponent(componentName);
278 startActivity(intent);
281 private void loadNewDocument(DocumentType docType) {
282 final String newDocumentType;
283 if (docType == DocumentType.WRITER) {
284 newDocumentType = NEW_WRITER_STRING_KEY;
285 } else if (docType == DocumentType.CALC) {
286 newDocumentType = NEW_CALC_STRING_KEY;
287 } else if (docType == DocumentType.IMPRESS) {
288 newDocumentType = NEW_IMPRESS_STRING_KEY;
289 } else if (docType == DocumentType.DRAW) {
290 newDocumentType = NEW_DRAW_STRING_KEY;
291 } else {
292 Log.w(LOGTAG, "invalid document type passed to loadNewDocument method. Ignoring request");
293 return;
296 Intent intent = new Intent(LibreOfficeUIActivity.this, LibreOfficeMainActivity.class);
297 intent.putExtra(NEW_DOC_TYPE_KEY, newDocumentType);
298 startActivity(intent);
301 @Override
302 public boolean onCreateOptionsMenu(Menu menu) {
303 MenuInflater inflater = getMenuInflater();
304 inflater.inflate(R.menu.view_menu, menu);
306 return true;
309 @Override
310 public boolean onOptionsItemSelected(MenuItem item) {
311 final int itemId = item.getItemId();
312 if (itemId == R.id.action_about) {
313 AboutDialogFragment aboutDialogFragment = new AboutDialogFragment();
314 aboutDialogFragment.show(getSupportFragmentManager(), "AboutDialogFragment");
315 return true;
317 if (itemId == R.id.action_settings) {
318 startActivity(new Intent(getApplicationContext(), SettingsActivity.class));
319 return true;
322 return super.onOptionsItemSelected(item);
325 @Override
326 protected void onResume() {
327 super.onResume();
328 Log.d(LOGTAG, "onResume");
329 createUI();
332 private void addDocumentToRecents(Uri fileUri) {
333 SharedPreferences prefs = getSharedPreferences(EXPLORER_PREFS_KEY, MODE_PRIVATE);
335 // preserve permissions across device reboots,
336 // s. https://developer.android.com/training/data-storage/shared/documents-files#persist-permissions
337 getContentResolver().takePersistableUriPermission(fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
339 String newRecent = fileUri.toString();
340 List<String> recentsList = new ArrayList<>(Arrays.asList(prefs.getString(RECENT_DOCUMENTS_KEY, "").split(RECENT_DOCUMENTS_DELIMITER)));
342 // remove string if present, so that it doesn't appear multiple times
343 recentsList.remove(newRecent);
345 // put the new value in the first place
346 recentsList.add(0, newRecent);
349 * 4 because the number of recommended items in App Shortcuts is 4, and also
350 * because it's a good number of recent items in general
352 final int RECENTS_SIZE = 4;
354 while (recentsList.size() > RECENTS_SIZE) {
355 recentsList.remove(RECENTS_SIZE);
358 // serialize to String that can be set for pref
359 String value = TextUtils.join(RECENT_DOCUMENTS_DELIMITER, recentsList);
360 prefs.edit().putString(RECENT_DOCUMENTS_KEY, value).apply();
362 //update app shortcuts (7.0 and above)
363 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
364 ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
366 //Remove all shortcuts, and apply new ones.
367 shortcutManager.removeAllDynamicShortcuts();
369 ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
370 for (String recentDoc : recentsList) {
371 Uri docUri = Uri.parse(recentDoc);
372 String filename = FileUtilities.retrieveDisplayNameForDocumentUri(getContentResolver(), docUri);
373 if (filename.isEmpty()) {
374 continue;
377 //find the appropriate drawable
378 int drawable = 0;
379 switch (FileUtilities.getType(filename)) {
380 case FileUtilities.DOC:
381 drawable = R.drawable.writer;
382 break;
383 case FileUtilities.CALC:
384 drawable = R.drawable.calc;
385 break;
386 case FileUtilities.DRAWING:
387 drawable = R.drawable.draw;
388 break;
389 case FileUtilities.IMPRESS:
390 drawable = R.drawable.impress;
391 break;
394 Intent intent = new Intent(Intent.ACTION_VIEW, docUri);
395 String packageName = this.getApplicationContext().getPackageName();
396 ComponentName componentName = new ComponentName(packageName, LibreOfficeMainActivity.class.getName());
397 intent.setComponent(componentName);
399 ShortcutInfo shortcut = new ShortcutInfo.Builder(this, filename)
400 .setShortLabel(filename)
401 .setLongLabel(filename)
402 .setIcon(Icon.createWithResource(this, drawable))
403 .setIntent(intent)
404 .build();
406 shortcuts.add(shortcut);
408 shortcutManager.setDynamicShortcuts(shortcuts);
412 @Override
413 public void onClick(View v) {
414 int id = v.getId();
415 if (id == R.id.editFAB) {
416 if (isFabMenuOpen) {
417 collapseFabMenu();
418 } else {
419 expandFabMenu();
421 } else if (id == R.id.open_file_button) {
422 showSystemFilePickerAndOpenFile();
423 } else if (id == R.id.newWriterFAB) {
424 loadNewDocument(DocumentType.WRITER);
425 } else if (id == R.id.newImpressFAB) {
426 loadNewDocument(DocumentType.IMPRESS);
427 } else if (id == R.id.newCalcFAB) {
428 loadNewDocument(DocumentType.CALC);
429 } else if (id == R.id.newDrawFAB) {
430 loadNewDocument(DocumentType.DRAW);
435 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */