1 /* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
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/.
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
{
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",
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",
109 "text/comma-separated-values",
110 "application/vnd.ms-works",
111 "application/vnd.apple.keynote",
112 "application/x-abiword",
113 "application/x-pagemaker",
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
;
138 public void onCreate(Bundle savedInstanceState
) {
139 super.onCreate(savedInstanceState
);
143 fabOpenAnimation
= AnimationUtils
.loadAnimation(this, R
.anim
.fab_open
);
144 fabCloseAnimation
= AnimationUtils
.loadAnimation(this, R
.anim
.fab_close
);
148 protected void 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;
238 public void onBackPressed() {
242 super.onBackPressed();
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
);
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
;
292 Log
.w(LOGTAG
, "invalid document type passed to loadNewDocument method. Ignoring request");
296 Intent intent
= new Intent(LibreOfficeUIActivity
.this, LibreOfficeMainActivity
.class);
297 intent
.putExtra(NEW_DOC_TYPE_KEY
, newDocumentType
);
298 startActivity(intent
);
302 public boolean onCreateOptionsMenu(Menu menu
) {
303 MenuInflater inflater
= getMenuInflater();
304 inflater
.inflate(R
.menu
.view_menu
, menu
);
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");
317 if (itemId
== R
.id
.action_settings
) {
318 startActivity(new Intent(getApplicationContext(), SettingsActivity
.class));
322 return super.onOptionsItemSelected(item
);
326 protected void onResume() {
328 Log
.d(LOGTAG
, "onResume");
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()) {
377 //find the appropriate drawable
379 switch (FileUtilities
.getType(filename
)) {
380 case FileUtilities
.DOC
:
381 drawable
= R
.drawable
.writer
;
383 case FileUtilities
.CALC
:
384 drawable
= R
.drawable
.calc
;
386 case FileUtilities
.DRAWING
:
387 drawable
= R
.drawable
.draw
;
389 case FileUtilities
.IMPRESS
:
390 drawable
= R
.drawable
.impress
;
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
))
406 shortcuts
.add(shortcut
);
408 shortcutManager
.setDynamicShortcuts(shortcuts
);
413 public void onClick(View v
) {
415 if (id
== R
.id
.editFAB
) {
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: */